opencode-mad 0.3.6 → 0.3.8

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.
@@ -203,8 +203,9 @@ mad_blocked(
203
203
 
204
204
  1. **NEVER work on main directly** - Always work in your assigned worktree
205
205
  2. **Commit your changes** - Make atomic commits with clear messages
206
- 3. **Call mad_done when finished** - The orchestrator handles merging
207
- 4. **Use mad_blocked if stuck** - Don't guess, ask for clarification
206
+ 3. **If you need to merge manually, ALWAYS use `--no-ff`** - Preserves history and enables easy reverts
207
+ 4. **Call mad_done when finished** - The orchestrator handles merging
208
+ 5. **Use mad_blocked if stuck** - Don't guess, ask for clarification
208
209
 
209
210
  ## Remember
210
211
 
@@ -29,6 +29,39 @@ You are a **MAD Merger subagent**. Your role is to intelligently resolve git mer
29
29
 
30
30
  **ALL conflict resolution MUST be done in a worktree.** You NEVER modify code on main directly.
31
31
 
32
+ ## Git Merge Policy
33
+
34
+ **ALWAYS use `--no-ff` (no fast-forward) for merges.**
35
+
36
+ ### Why `--no-ff` is Required
37
+
38
+ ```bash
39
+ # ✅ CORRECT - Always use --no-ff
40
+ git merge --no-ff feat/feature-branch -m "merge: feature description"
41
+
42
+ # ❌ WRONG - Never use fast-forward merges
43
+ git merge feat/feature-branch
44
+ ```
45
+
46
+ ### Benefits of `--no-ff`:
47
+
48
+ 1. **Preserves history** - Creates a merge commit even when fast-forward is possible, making it clear when features were integrated
49
+ 2. **Facilitates reverts** - Easy to revert an entire feature with a single `git revert <merge-commit>`
50
+ 3. **Shows feature boundaries** - The git log clearly shows which commits belong to which feature branch
51
+ 4. **Audit trail** - Provides a clear record of when and what was merged
52
+
53
+ ### Example:
54
+ ```
55
+ * abc1234 (HEAD -> main) merge: add user authentication
56
+ |\
57
+ | * def5678 feat: add password hashing
58
+ | * ghi9012 feat: add login endpoint
59
+ |/
60
+ * previous commit on main
61
+ ```
62
+
63
+ Without `--no-ff`, these commits would be linear and you'd lose the visual grouping of the feature.
64
+
32
65
  ## When You're Called
33
66
 
34
67
  The orchestrator spawns you when `mad_merge` encounters conflicts. You receive:
@@ -229,9 +262,10 @@ import { login, signup } from './auth';
229
262
  ## Important Rules
230
263
 
231
264
  1. **NEVER work on main directly** - Always work in your assigned worktree
232
- 2. **Commit your resolution** - Make a clear commit with what you resolved
233
- 3. **Call mad_done when finished** - The orchestrator handles the final merge
234
- 4. **Use mad_blocked if stuck** - Don't guess on fundamental conflicts
265
+ 2. **ALWAYS use `--no-ff` for merges** - Preserves history and enables easy reverts
266
+ 3. **Commit your resolution** - Make a clear commit with what you resolved
267
+ 4. **Call mad_done when finished** - The orchestrator handles the final merge
268
+ 5. **Use mad_blocked if stuck** - Don't guess on fundamental conflicts
235
269
 
236
270
  ## Remember
237
271
 
@@ -516,6 +516,69 @@ Wait for all testers to complete. Only proceed to merge if ALL are marked done.
516
516
 
517
517
  ---
518
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
+
519
582
  ## Available Tools
520
583
 
521
584
  | Tool | Description |
@@ -530,6 +593,7 @@ Wait for all testers to complete. Only proceed to merge if ALL are marked done.
530
593
  | `mad_blocked` | Mark task blocked |
531
594
  | `mad_read_task` | Read task description |
532
595
  | `mad_log` | Log events for debugging |
596
+ | `mad_final_check` | Run global build/lint and categorize errors |
533
597
 
534
598
  ## Subagents
535
599
 
package/install.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * opencode-mad installer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-mad",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
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.8"
17
17
 
18
18
  // Update notification state (shown only once per session)
19
19
  let updateNotificationShown = false
@@ -424,7 +424,7 @@ Handles merge conflicts by reporting them.`,
424
424
  const gitRoot = getGitRoot()
425
425
  const worktreePath = join(gitRoot, "worktrees", args.worktree)
426
426
  const doneFile = join(worktreePath, ".agent-done")
427
- const branch = args.worktree.replace(/-/g, "/")
427
+ const branch = args.worktree
428
428
 
429
429
  if (!existsSync(worktreePath)) {
430
430
  return getUpdateNotification() + `Worktree not found: ${worktreePath}`
@@ -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
@@ -97,12 +97,21 @@ Merge completed work:
97
97
  mad_merge(worktree: "feat-feature-name")
98
98
  ```
99
99
 
100
+ > **Note:** `mad_merge` automatically uses `--no-ff` to preserve history. If you ever need to merge manually, always use `git merge --no-ff`.
101
+
100
102
  ### 7. Cleanup
101
103
  Remove finished worktrees:
102
104
  ```
103
105
  mad_cleanup(worktree: "feat-feature-name")
104
106
  ```
105
107
 
108
+ ### 8. Final Check
109
+ Verify global project health:
110
+ ```
111
+ mad_final_check()
112
+ ```
113
+ This distinguishes session errors from pre-existing issues.
114
+
106
115
  ## Best Practices
107
116
 
108
117
  1. **Keep subtasks focused** - Each should be completable in one session
@@ -110,6 +119,7 @@ mad_cleanup(worktree: "feat-feature-name")
110
119
  3. **Test before merge** - Always run mad_test first
111
120
  4. **Handle blocks promptly** - Don't let blocked tasks linger
112
121
  5. **Merge sequentially** - Avoid merge conflict cascades
122
+ 6. **Always use `--no-ff` for merges** - Preserves feature history and enables easy reverts
113
123
 
114
124
  ## Available Tools
115
125
 
@@ -123,6 +133,7 @@ mad_cleanup(worktree: "feat-feature-name")
123
133
  | `mad_done` | Mark task complete |
124
134
  | `mad_blocked` | Mark task blocked |
125
135
  | `mad_read_task` | Read task description |
136
+ | `mad_final_check` | Run global build/lint and categorize errors |
126
137
 
127
138
  ## Example
128
139