opencode-mad 0.3.5 → 0.3.7

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.
@@ -5,15 +5,7 @@ model: anthropic/claude-opus-4-5
5
5
  temperature: 0.3
6
6
  color: "#9333ea"
7
7
  permission:
8
- task:
9
- "*": allow
10
- bash:
11
- "*": allow
12
- read:
13
- "*": allow
14
- glob:
15
- "*": allow
16
- grep:
8
+ "*":
17
9
  "*": allow
18
10
  tools:
19
11
  mad_worktree_create: true
@@ -524,6 +516,69 @@ Wait for all testers to complete. Only proceed to merge if ALL are marked done.
524
516
 
525
517
  ---
526
518
 
519
+ ## Phase 5.5: Final Global Check
520
+
521
+ **IMPORTANT: Run this after all merges are complete!**
522
+
523
+ Use `mad_final_check` to verify the entire project's build and lint status:
524
+
525
+ ```
526
+ mad_final_check()
527
+ ```
528
+
529
+ This will:
530
+ 1. Run all configured build/lint commands (npm run build, npm run lint, etc.)
531
+ 2. Compare any errors against files modified during the session
532
+ 3. Categorize errors as "session errors" or "pre-existing errors"
533
+
534
+ ### Handling Results
535
+
536
+ #### If session errors are found:
537
+ These are bugs introduced by the MAD session. Create a fix worktree:
538
+
539
+ ```
540
+ mad_worktree_create(
541
+ branch: "fix-session-errors",
542
+ task: "Fix build/lint errors introduced during session:
543
+ [list of errors]
544
+
545
+ YOU OWN ALL FILES in this worktree."
546
+ )
547
+
548
+ Task(
549
+ subagent_type: "mad-fixer",
550
+ description: "Fix session errors",
551
+ prompt: "Work in worktree 'fix-session-errors'. Fix the build/lint errors, commit, and call mad_done."
552
+ )
553
+ ```
554
+
555
+ #### If only pre-existing errors are found:
556
+ These are NOT caused by the session. Inform the user:
557
+
558
+ ```
559
+ "Your session completed successfully! No new errors were introduced.
560
+
561
+ However, I found [N] pre-existing build/lint errors that were already in the codebase.
562
+ Would you like me to fix them? (Note: these are not caused by our changes today)"
563
+ ```
564
+
565
+ If the user says yes, create a worktree to fix them:
566
+
567
+ ```
568
+ mad_worktree_create(
569
+ branch: "fix-preexisting-errors",
570
+ task: "Fix pre-existing build/lint errors (NOT from this session):
571
+ [list of errors]
572
+
573
+ These errors existed before the MAD session started."
574
+ )
575
+ ```
576
+
577
+ #### If no errors:
578
+ Celebrate! The project is clean.
579
+
580
+ ---
581
+
527
582
  ## Available Tools
528
583
 
529
584
  | Tool | Description |
@@ -538,6 +593,7 @@ Wait for all testers to complete. Only proceed to merge if ALL are marked done.
538
593
  | `mad_blocked` | Mark task blocked |
539
594
  | `mad_read_task` | Read task description |
540
595
  | `mad_log` | Log events for debugging |
596
+ | `mad_final_check` | Run global build/lint and categorize errors |
541
597
 
542
598
  ## Subagents
543
599
 
package/install.js CHANGED
@@ -55,32 +55,22 @@ const folders = ['agents', 'commands', 'plugins', 'skills'];
55
55
  // Check if it's an update (any of the folders already exist)
56
56
  const isUpdate = folders.some(folder => existsSync(join(targetDir, folder)));
57
57
 
58
- console.log(isUpdate
59
- ? `\nšŸ”„ Updating opencode-mad in ${targetDir}\n`
60
- : `\nšŸš€ Installing opencode-mad to ${targetDir}\n`);
58
+ // Get version from package.json
59
+ const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf-8'));
60
+ const version = pkg.version;
61
61
 
62
+ // Copy folders silently
62
63
  for (const folder of folders) {
63
64
  const src = join(__dirname, folder);
64
65
  const dest = join(targetDir, folder);
65
66
 
66
- if (!existsSync(src)) {
67
- console.log(`āš ļø Skipping ${folder} (not found)`);
68
- continue;
69
- }
67
+ if (!existsSync(src)) continue;
70
68
 
71
69
  mkdirSync(dest, { recursive: true });
72
70
  cpSync(src, dest, { recursive: true });
73
- console.log(`āœ… Copied ${folder}/`);
74
71
  }
75
72
 
76
- console.log(`
77
- šŸŽ‰ ${isUpdate ? 'Update' : 'Installation'} complete!
78
-
79
- ${isGlobal ? 'MAD is now available in all your projects.' : 'MAD is now available in this project.'}
80
-
81
- Just start talking to the orchestrator:
82
- "Create a full-stack app with Express and React"
83
-
84
- Or use the /mad command:
85
- /mad Create a Task Timer app
86
- `);
73
+ // Single line output
74
+ const action = isUpdate ? 'updated' : 'installed';
75
+ const location = isGlobal ? '~/.config/opencode' : '.opencode';
76
+ console.log(`opencode-mad v${version} ${action} to ${location}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-mad",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "Multi-Agent Dev - Parallel development orchestration plugin for OpenCode",
5
5
  "type": "module",
6
6
  "main": "plugins/mad-plugin.ts",
@@ -13,7 +13,7 @@ import { execSync } from "child_process"
13
13
  */
14
14
 
15
15
  // Current version of opencode-mad
16
- const CURRENT_VERSION = "0.3.4"
16
+ const CURRENT_VERSION = "0.3.7"
17
17
 
18
18
  // Update notification state (shown only once per session)
19
19
  let updateNotificationShown = false
@@ -728,6 +728,269 @@ Latest version: ${updateInfo.latest}`
728
728
  }
729
729
  },
730
730
  }),
731
+
732
+ /**
733
+ * Final check - run global build/lint and categorize errors
734
+ */
735
+ mad_final_check: tool({
736
+ description: `Run global build/lint checks on the main project after all merges.
737
+ Compares errors against files modified during the MAD session to distinguish:
738
+ - Session errors: caused by changes made during this session
739
+ - Pre-existing errors: already present before the session started
740
+
741
+ Use this at the end of the MAD workflow to ensure code quality.`,
742
+ args: {
743
+ baseCommit: tool.schema.string().optional().describe("The commit SHA from before the MAD session started. If not provided, will try to detect from reflog."),
744
+ },
745
+ async execute(args, context) {
746
+ try {
747
+ const gitRoot = getGitRoot()
748
+
749
+ // 1. Determine base commit for comparison
750
+ let baseCommit = args.baseCommit
751
+ if (!baseCommit) {
752
+ // Try to find the commit before MAD session started (look for last commit before worktrees were created)
753
+ const reflogResult = runCommand('git reflog --format="%H %gs" -n 50', gitRoot)
754
+ if (reflogResult.success) {
755
+ // Find first commit that's not a merge from a MAD branch
756
+ const lines = reflogResult.output.split('\n')
757
+ for (const line of lines) {
758
+ if (!line.includes('merge') || (!line.includes('feat-') && !line.includes('fix-'))) {
759
+ baseCommit = line.split(' ')[0]
760
+ break
761
+ }
762
+ }
763
+ }
764
+ if (!baseCommit) {
765
+ baseCommit = 'HEAD~10' // Fallback
766
+ }
767
+ }
768
+
769
+ // 2. Get list of files modified during session
770
+ const diffResult = runCommand(`git diff ${baseCommit}..HEAD --name-only`, gitRoot)
771
+ const modifiedFiles = diffResult.success
772
+ ? diffResult.output.split('\n').filter(f => f.trim()).map(f => f.trim())
773
+ : []
774
+
775
+ let report = getUpdateNotification() + `# Final Project Check\n\n`
776
+ report += `šŸ“Š **Session Summary:**\n`
777
+ report += `- Base commit: \`${baseCommit.substring(0, 8)}\`\n`
778
+ report += `- Files modified: ${modifiedFiles.length}\n\n`
779
+
780
+ // 3. Detect project type and run checks
781
+ const packageJson = join(gitRoot, "package.json")
782
+ const goMod = join(gitRoot, "go.mod")
783
+ const cargoToml = join(gitRoot, "Cargo.toml")
784
+ const pyProject = join(gitRoot, "pyproject.toml")
785
+
786
+ interface CheckError {
787
+ file: string
788
+ line?: number
789
+ message: string
790
+ isSessionError: boolean
791
+ }
792
+
793
+ const allErrors: CheckError[] = []
794
+ let checksRun = 0
795
+
796
+ // Helper to parse errors and categorize them
797
+ const parseAndCategorize = (output: string, checkName: string) => {
798
+ // Common patterns for file:line:message
799
+ const patterns = [
800
+ /^(.+?):(\d+):\d*:?\s*(.+)$/gm, // file:line:col: message
801
+ /^(.+?)\((\d+),\d+\):\s*(.+)$/gm, // file(line,col): message (TypeScript)
802
+ /^\s*(.+?):(\d+)\s+(.+)$/gm, // file:line message
803
+ ]
804
+
805
+ for (const pattern of patterns) {
806
+ let match
807
+ while ((match = pattern.exec(output)) !== null) {
808
+ const file = match[1].trim().replace(/\\/g, '/')
809
+ const line = parseInt(match[2])
810
+ const message = match[3].trim()
811
+
812
+ // Check if this file was modified during session
813
+ const isSessionError = modifiedFiles.some(mf =>
814
+ file.endsWith(mf) || mf.endsWith(file) || file.includes(mf) || mf.includes(file)
815
+ )
816
+
817
+ allErrors.push({ file, line, message, isSessionError })
818
+ }
819
+ }
820
+ }
821
+
822
+ // Run checks based on project type
823
+ if (existsSync(packageJson)) {
824
+ const pkg = JSON.parse(readFileSync(packageJson, "utf-8"))
825
+
826
+ if (pkg.scripts?.lint) {
827
+ checksRun++
828
+ report += `## šŸ” Lint Check\n`
829
+ const lintResult = runCommand("npm run lint 2>&1", gitRoot)
830
+ if (lintResult.success) {
831
+ report += `āœ… Lint passed\n\n`
832
+ } else {
833
+ report += `āŒ Lint failed\n`
834
+ parseAndCategorize(lintResult.error || lintResult.output, "lint")
835
+ }
836
+ }
837
+
838
+ if (pkg.scripts?.build) {
839
+ checksRun++
840
+ report += `## šŸ”Ø Build Check\n`
841
+ const buildResult = runCommand("npm run build 2>&1", gitRoot)
842
+ if (buildResult.success) {
843
+ report += `āœ… Build passed\n\n`
844
+ } else {
845
+ report += `āŒ Build failed\n`
846
+ parseAndCategorize(buildResult.error || buildResult.output, "build")
847
+ }
848
+ }
849
+
850
+ if (pkg.scripts?.typecheck || pkg.scripts?.["type-check"]) {
851
+ checksRun++
852
+ const cmd = pkg.scripts?.typecheck ? "npm run typecheck" : "npm run type-check"
853
+ report += `## šŸ“ TypeCheck\n`
854
+ const tcResult = runCommand(`${cmd} 2>&1`, gitRoot)
855
+ if (tcResult.success) {
856
+ report += `āœ… TypeCheck passed\n\n`
857
+ } else {
858
+ report += `āŒ TypeCheck failed\n`
859
+ parseAndCategorize(tcResult.error || tcResult.output, "typecheck")
860
+ }
861
+ }
862
+ }
863
+
864
+ if (existsSync(goMod)) {
865
+ checksRun++
866
+ report += `## šŸ”Ø Go Build\n`
867
+ const goBuild = runCommand("go build ./... 2>&1", gitRoot)
868
+ if (goBuild.success) {
869
+ report += `āœ… Go build passed\n\n`
870
+ } else {
871
+ parseAndCategorize(goBuild.error || goBuild.output, "go build")
872
+ }
873
+
874
+ checksRun++
875
+ report += `## šŸ” Go Vet\n`
876
+ const goVet = runCommand("go vet ./... 2>&1", gitRoot)
877
+ if (goVet.success) {
878
+ report += `āœ… Go vet passed\n\n`
879
+ } else {
880
+ parseAndCategorize(goVet.error || goVet.output, "go vet")
881
+ }
882
+ }
883
+
884
+ if (existsSync(cargoToml)) {
885
+ checksRun++
886
+ report += `## šŸ”Ø Cargo Check\n`
887
+ const cargoCheck = runCommand("cargo check 2>&1", gitRoot)
888
+ if (cargoCheck.success) {
889
+ report += `āœ… Cargo check passed\n\n`
890
+ } else {
891
+ parseAndCategorize(cargoCheck.error || cargoCheck.output, "cargo")
892
+ }
893
+
894
+ checksRun++
895
+ report += `## šŸ” Cargo Clippy\n`
896
+ const clippy = runCommand("cargo clippy 2>&1", gitRoot)
897
+ if (clippy.success) {
898
+ report += `āœ… Clippy passed\n\n`
899
+ } else {
900
+ parseAndCategorize(clippy.error || clippy.output, "clippy")
901
+ }
902
+ }
903
+
904
+ if (existsSync(pyProject)) {
905
+ checksRun++
906
+ report += `## šŸ” Python Lint (ruff/flake8)\n`
907
+ let pyLint = runCommand("ruff check . 2>&1", gitRoot)
908
+ if (!pyLint.success && pyLint.error?.includes("not found")) {
909
+ pyLint = runCommand("flake8 . 2>&1", gitRoot)
910
+ }
911
+ if (pyLint.success) {
912
+ report += `āœ… Python lint passed\n\n`
913
+ } else {
914
+ parseAndCategorize(pyLint.error || pyLint.output, "python lint")
915
+ }
916
+
917
+ checksRun++
918
+ report += `## šŸ“ Python Type Check (mypy)\n`
919
+ const mypy = runCommand("mypy . 2>&1", gitRoot)
920
+ if (mypy.success) {
921
+ report += `āœ… Mypy passed\n\n`
922
+ } else {
923
+ parseAndCategorize(mypy.error || mypy.output, "mypy")
924
+ }
925
+ }
926
+
927
+ if (checksRun === 0) {
928
+ report += `āš ļø No build/lint scripts detected in this project.\n`
929
+ report += `Supported: package.json (npm), go.mod, Cargo.toml, pyproject.toml\n`
930
+ logEvent("warn", "mad_final_check: no checks detected", { gitRoot })
931
+ return report
932
+ }
933
+
934
+ // 4. Categorize and report errors
935
+ const sessionErrors = allErrors.filter(e => e.isSessionError)
936
+ const preExistingErrors = allErrors.filter(e => !e.isSessionError)
937
+
938
+ report += `---\n\n## šŸ“‹ Error Summary\n\n`
939
+
940
+ if (allErrors.length === 0) {
941
+ report += `šŸŽ‰ **All checks passed!** No errors detected.\n`
942
+ logEvent("info", "mad_final_check: all checks passed", { checksRun })
943
+ return report
944
+ }
945
+
946
+ if (sessionErrors.length > 0) {
947
+ report += `### āŒ Session Errors (${sessionErrors.length})\n`
948
+ report += `*These errors are in files modified during this session:*\n\n`
949
+ for (const err of sessionErrors.slice(0, 10)) {
950
+ report += `- \`${err.file}${err.line ? `:${err.line}` : ''}\`: ${err.message.substring(0, 100)}\n`
951
+ }
952
+ if (sessionErrors.length > 10) {
953
+ report += `- ... and ${sessionErrors.length - 10} more\n`
954
+ }
955
+ report += `\n`
956
+ }
957
+
958
+ if (preExistingErrors.length > 0) {
959
+ report += `### āš ļø Pre-existing Errors (${preExistingErrors.length})\n`
960
+ report += `*These errors are NOT caused by this session - they existed before:*\n\n`
961
+ for (const err of preExistingErrors.slice(0, 10)) {
962
+ report += `- \`${err.file}${err.line ? `:${err.line}` : ''}\`: ${err.message.substring(0, 100)}\n`
963
+ }
964
+ if (preExistingErrors.length > 10) {
965
+ report += `- ... and ${preExistingErrors.length - 10} more\n`
966
+ }
967
+ report += `\n`
968
+ report += `šŸ’” **These pre-existing errors are not your fault!**\n`
969
+ report += `Would you like me to create a worktree to fix them? Just say "fix pre-existing errors".\n`
970
+ }
971
+
972
+ // 5. Final verdict
973
+ report += `\n---\n\n`
974
+ if (sessionErrors.length > 0) {
975
+ report += `āš ļø **Action required:** Fix the ${sessionErrors.length} session error(s) before considering this session complete.\n`
976
+ } else if (preExistingErrors.length > 0) {
977
+ report += `āœ… **Session successful!** Your changes introduced no new errors.\n`
978
+ report += `The ${preExistingErrors.length} pre-existing error(s) can be fixed separately if desired.\n`
979
+ }
980
+
981
+ logEvent("info", "mad_final_check completed", {
982
+ checksRun,
983
+ sessionErrors: sessionErrors.length,
984
+ preExistingErrors: preExistingErrors.length
985
+ })
986
+
987
+ return report
988
+ } catch (e: any) {
989
+ logEvent("error", "mad_final_check exception", { error: e.message, stack: e.stack })
990
+ return getUpdateNotification() + `āŒ Error running final check: ${e.message}`
991
+ }
992
+ },
993
+ }),
731
994
  },
732
995
 
733
996
  // Event hooks
@@ -103,6 +103,13 @@ Remove finished worktrees:
103
103
  mad_cleanup(worktree: "feat-feature-name")
104
104
  ```
105
105
 
106
+ ### 8. Final Check
107
+ Verify global project health:
108
+ ```
109
+ mad_final_check()
110
+ ```
111
+ This distinguishes session errors from pre-existing issues.
112
+
106
113
  ## Best Practices
107
114
 
108
115
  1. **Keep subtasks focused** - Each should be completable in one session
@@ -123,6 +130,7 @@ mad_cleanup(worktree: "feat-feature-name")
123
130
  | `mad_done` | Mark task complete |
124
131
  | `mad_blocked` | Mark task blocked |
125
132
  | `mad_read_task` | Read task description |
133
+ | `mad_final_check` | Run global build/lint and categorize errors |
126
134
 
127
135
  ## Example
128
136