opencode-mad 0.4.0 → 1.0.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-mad",
3
- "version": "0.4.0",
3
+ "version": "1.0.0",
4
4
  "description": "Multi-Agent Dev - Parallel development orchestration plugin for OpenCode",
5
5
  "type": "module",
6
6
  "main": "plugins/mad-plugin.ts",
@@ -12,8 +12,39 @@ import { execSync } from "child_process"
12
12
  * running in parallel via OpenCode's Task tool.
13
13
  */
14
14
 
15
+ // Types for agent permissions (constraint enforcement)
16
+ interface AgentPermissions {
17
+ type: 'orchestrator' | 'analyste' | 'architecte' | 'developer' | 'tester' | 'reviewer' | 'fixer' | 'merger' | 'security'
18
+ canEdit: boolean
19
+ canWrite: boolean
20
+ canPatch: boolean
21
+ allowedPaths: string[] | null // null = all, [] = none, [...] = specific list
22
+ deniedPaths: string[] // Explicitly denied paths
23
+ worktree: string | null // Worktree path if applicable
24
+ }
25
+
26
+ // Global map to store permissions by sessionID
27
+ const agentPermissions = new Map<string, AgentPermissions>()
28
+
29
+ // Simple glob matching (for patterns like /backend/**)
30
+ function matchGlob(path: string, pattern: string): boolean {
31
+ // Normalize paths
32
+ const normalizedPath = path.replace(/\\/g, '/')
33
+ const normalizedPattern = pattern.replace(/\\/g, '/')
34
+
35
+ // Convert glob to regex
36
+ const regexPattern = normalizedPattern
37
+ .replace(/\*\*/g, '{{DOUBLESTAR}}')
38
+ .replace(/\*/g, '[^/]*')
39
+ .replace(/{{DOUBLESTAR}}/g, '.*')
40
+ .replace(/\//g, '\\/')
41
+
42
+ const regex = new RegExp(`^${regexPattern}$`)
43
+ return regex.test(normalizedPath)
44
+ }
45
+
15
46
  // Current version of opencode-mad
16
- const CURRENT_VERSION = "0.4.0"
47
+ const CURRENT_VERSION = "1.0.0"
17
48
 
18
49
  // Update notification state (shown only once per session)
19
50
  let updateNotificationShown = false
@@ -21,6 +52,9 @@ let pendingUpdateMessage: string | null = null
21
52
 
22
53
  export const MADPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
23
54
 
55
+ // Use the directory provided by OpenCode, fallback to process.cwd() for backwards compatibility
56
+ const baseDirectory = directory || process.cwd()
57
+
24
58
  /**
25
59
  * Helper to run shell commands with proper error handling (cross-platform)
26
60
  */
@@ -28,7 +62,7 @@ export const MADPlugin: Plugin = async ({ project, client, $, directory, worktre
28
62
  try {
29
63
  const output = execSync(cmd, {
30
64
  encoding: "utf-8",
31
- cwd: cwd || process.cwd(),
65
+ cwd: cwd || baseDirectory,
32
66
  stdio: ["pipe", "pipe", "pipe"]
33
67
  })
34
68
  return { success: true, output: output.trim() }
@@ -43,9 +77,10 @@ export const MADPlugin: Plugin = async ({ project, client, $, directory, worktre
43
77
 
44
78
  /**
45
79
  * Helper to get git root with error handling
80
+ * @param basePath - Optional base path to start from (defaults to baseDirectory)
46
81
  */
47
- const getGitRoot = (): string => {
48
- const result = runCommand("git rev-parse --show-toplevel")
82
+ const getGitRoot = (basePath?: string): string => {
83
+ const result = runCommand("git rev-parse --show-toplevel", basePath || baseDirectory)
49
84
  if (!result.success) {
50
85
  throw new Error(`Not a git repository or git not found: ${result.error}`)
51
86
  }
@@ -54,18 +89,20 @@ export const MADPlugin: Plugin = async ({ project, client, $, directory, worktre
54
89
 
55
90
  /**
56
91
  * Helper to get current branch with fallback
92
+ * @param basePath - Optional base path to run git command from (defaults to baseDirectory)
57
93
  */
58
- const getCurrentBranch = (): string => {
59
- const result = runCommand("git symbolic-ref --short HEAD")
94
+ const getCurrentBranch = (basePath?: string): string => {
95
+ const result = runCommand("git symbolic-ref --short HEAD", basePath || baseDirectory)
60
96
  return result.success ? result.output : "main"
61
97
  }
62
98
 
63
99
  /**
64
100
  * Helper to log MAD events
101
+ * Uses baseDirectory to find the git root for log file location
65
102
  */
66
103
  const logEvent = (level: "info" | "warn" | "error" | "debug", message: string, context?: any) => {
67
104
  try {
68
- const gitRoot = getGitRoot()
105
+ const gitRoot = getGitRoot(baseDirectory)
69
106
  const logFile = join(gitRoot, ".mad-logs.jsonl")
70
107
  const logEntry = JSON.stringify({
71
108
  timestamp: new Date().toISOString(),
@@ -1111,6 +1148,306 @@ Use this at the end of the MAD workflow to ensure code quality.`,
1111
1148
  }
1112
1149
  },
1113
1150
  }),
1151
+
1152
+ /**
1153
+ * Register agent permissions for constraint enforcement
1154
+ */
1155
+ mad_register_agent: tool({
1156
+ description: `Register an agent's permissions for constraint enforcement.
1157
+ Call this when spawning a subagent to define what it can and cannot do.
1158
+ The plugin will then BLOCK any unauthorized actions.`,
1159
+ args: {
1160
+ sessionID: tool.schema.string().describe("The session ID of the agent"),
1161
+ agentType: tool.schema.enum([
1162
+ 'orchestrator', 'analyste', 'architecte', 'developer',
1163
+ 'tester', 'reviewer', 'fixer', 'merger', 'security'
1164
+ ]).describe("Type of agent"),
1165
+ worktree: tool.schema.string().optional().describe("Worktree path if applicable"),
1166
+ allowedPaths: tool.schema.array(tool.schema.string()).optional().describe("Paths the agent can edit (glob patterns)"),
1167
+ deniedPaths: tool.schema.array(tool.schema.string()).optional().describe("Paths explicitly denied"),
1168
+ },
1169
+ async execute(args, context) {
1170
+ const { sessionID, agentType, worktree, allowedPaths, deniedPaths } = args
1171
+
1172
+ // Define default permissions based on agent type
1173
+ const readOnlyAgents = ['orchestrator', 'analyste', 'architecte', 'tester', 'reviewer', 'security']
1174
+ const canEdit = !readOnlyAgents.includes(agentType)
1175
+
1176
+ const permissions: AgentPermissions = {
1177
+ type: agentType,
1178
+ canEdit,
1179
+ canWrite: canEdit,
1180
+ canPatch: canEdit,
1181
+ allowedPaths: allowedPaths || null,
1182
+ deniedPaths: deniedPaths || [],
1183
+ worktree: worktree || null,
1184
+ }
1185
+
1186
+ agentPermissions.set(sessionID, permissions)
1187
+
1188
+ logEvent("info", `Registered agent permissions`, { sessionID, agentType, canEdit, worktree })
1189
+
1190
+ return getUpdateNotification() + `✅ Agent registered: ${agentType} (canEdit: ${canEdit}, worktree: ${worktree || 'none'})`
1191
+ }
1192
+ }),
1193
+
1194
+ /**
1195
+ * Analyze codebase - for Analyst agent
1196
+ */
1197
+ mad_analyze: tool({
1198
+ description: `Trigger a codebase analysis. Returns a structured report.
1199
+ Use mode 'full' for complete project scan, 'targeted' for task-specific analysis.`,
1200
+ args: {
1201
+ mode: tool.schema.enum(['full', 'targeted']).describe("Analysis mode"),
1202
+ focus: tool.schema.string().optional().describe("For targeted mode: what to focus on"),
1203
+ paths: tool.schema.array(tool.schema.string()).optional().describe("Specific paths to analyze"),
1204
+ },
1205
+ async execute(args, context) {
1206
+ const { mode, focus, paths } = args
1207
+ const gitRoot = getGitRoot()
1208
+
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
+ const structure = runCommand('find . -type f -name "*.ts" -o -name "*.js" -o -name "*.json" | grep -v node_modules | head -50', gitRoot)
1215
+ 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
+ }
1229
+
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
+ }
1236
+
1237
+ logEvent("info", "Codebase analysis completed", { mode, focus })
1238
+ return getUpdateNotification() + report
1239
+ }
1240
+ }),
1241
+
1242
+ /**
1243
+ * Unregister agent permissions when it completes
1244
+ */
1245
+ mad_unregister_agent: tool({
1246
+ description: `Unregister an agent's permissions when it completes.`,
1247
+ args: {
1248
+ sessionID: tool.schema.string().describe("The session ID to unregister"),
1249
+ },
1250
+ async execute(args) {
1251
+ const existed = agentPermissions.has(args.sessionID)
1252
+ 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
+ }
1260
+ }
1261
+ }),
1262
+
1263
+ /**
1264
+ * Create development plan - for Architect agent
1265
+ */
1266
+ mad_create_plan: tool({
1267
+ description: `Store a development plan created by the Architect agent.
1268
+ The plan will be available for the orchestrator to present to the user.`,
1269
+ args: {
1270
+ planName: tool.schema.string().describe("Name/identifier for the plan"),
1271
+ plan: tool.schema.string().describe("The full development plan in markdown"),
1272
+ tasks: tool.schema.array(tool.schema.object({
1273
+ name: tool.schema.string(),
1274
+ branch: tool.schema.string(),
1275
+ ownership: tool.schema.array(tool.schema.string()),
1276
+ denied: tool.schema.array(tool.schema.string()).optional(),
1277
+ dependencies: tool.schema.array(tool.schema.string()).optional(),
1278
+ })).describe("Structured task list"),
1279
+ },
1280
+ async execute(args, context) {
1281
+ const { planName, plan, tasks } = args
1282
+
1283
+ // Stocker le plan en mémoire (pourrait être persisté plus tard)
1284
+ const planData = {
1285
+ name: planName,
1286
+ createdAt: new Date().toISOString(),
1287
+ plan,
1288
+ tasks,
1289
+ }
1290
+
1291
+ // Log pour debugging
1292
+ logEvent("info", "Development plan created", { planName, taskCount: tasks.length })
1293
+
1294
+ return getUpdateNotification() + `✅ Plan '${planName}' created with ${tasks.length} tasks.\n\n${plan}`
1295
+ }
1296
+ }),
1297
+
1298
+ /**
1299
+ * Submit code review - for Reviewer agent
1300
+ */
1301
+ mad_review: tool({
1302
+ description: `Submit a code review report for a worktree.
1303
+ Called by the Reviewer agent after analyzing the code.`,
1304
+ args: {
1305
+ worktree: tool.schema.string().describe("Worktree that was reviewed"),
1306
+ verdict: tool.schema.enum(['approved', 'changes_requested', 'rejected']).describe("Review verdict"),
1307
+ summary: tool.schema.string().describe("Brief summary of the review"),
1308
+ issues: tool.schema.array(tool.schema.object({
1309
+ severity: tool.schema.enum(['critical', 'major', 'minor']),
1310
+ file: tool.schema.string(),
1311
+ line: tool.schema.number().optional(),
1312
+ message: tool.schema.string(),
1313
+ suggestion: tool.schema.string().optional(),
1314
+ })).optional().describe("List of issues found"),
1315
+ positives: tool.schema.array(tool.schema.string()).optional().describe("Positive aspects of the code"),
1316
+ },
1317
+ async execute(args, context) {
1318
+ const { worktree, verdict, summary, issues, positives } = args
1319
+ const gitRoot = getGitRoot()
1320
+ const worktreePath = join(gitRoot, "worktrees", worktree)
1321
+
1322
+ if (!existsSync(worktreePath)) {
1323
+ return getUpdateNotification() + `❌ Worktree not found: ${worktreePath}`
1324
+ }
1325
+
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`
1330
+
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
+ }
1364
+
1365
+ // Sauvegarder le rapport dans le worktree
1366
+ writeFileSync(join(worktreePath, '.agent-review'), report)
1367
+
1368
+ logEvent("info", "Code review submitted", { worktree, verdict, issueCount: issues?.length || 0 })
1369
+
1370
+ return getUpdateNotification() + report
1371
+ }
1372
+ }),
1373
+
1374
+ /**
1375
+ * Security scan - for Security agent
1376
+ */
1377
+ mad_security_scan: tool({
1378
+ description: `Submit a security scan report for a worktree or the main project.
1379
+ Called by the Security agent after scanning for vulnerabilities.`,
1380
+ args: {
1381
+ target: tool.schema.string().describe("Worktree name or 'main' for main project"),
1382
+ riskLevel: tool.schema.enum(['low', 'medium', 'high', 'critical']).describe("Overall risk level"),
1383
+ summary: tool.schema.string().describe("Brief summary of findings"),
1384
+ vulnerabilities: tool.schema.array(tool.schema.object({
1385
+ id: tool.schema.string(),
1386
+ severity: tool.schema.enum(['low', 'medium', 'high', 'critical']),
1387
+ type: tool.schema.string(),
1388
+ file: tool.schema.string().optional(),
1389
+ line: tool.schema.number().optional(),
1390
+ description: tool.schema.string(),
1391
+ remediation: tool.schema.string(),
1392
+ })).optional().describe("List of vulnerabilities found"),
1393
+ dependencyIssues: tool.schema.array(tool.schema.object({
1394
+ package: tool.schema.string(),
1395
+ severity: tool.schema.string(),
1396
+ cve: tool.schema.string().optional(),
1397
+ fix: tool.schema.string(),
1398
+ })).optional().describe("Vulnerable dependencies"),
1399
+ },
1400
+ async execute(args, context) {
1401
+ const { target, riskLevel, summary, vulnerabilities, dependencyIssues } = args
1402
+ const gitRoot = getGitRoot()
1403
+
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
1432
+ 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
+
1438
+ // Sauvegarder si c'est un worktree
1439
+ if (target !== 'main') {
1440
+ const worktreePath = join(gitRoot, "worktrees", target)
1441
+ if (existsSync(worktreePath)) {
1442
+ writeFileSync(join(worktreePath, '.agent-security'), report)
1443
+ }
1444
+ }
1445
+
1446
+ logEvent("info", "Security scan completed", { target, riskLevel, vulnCount: vulnerabilities?.length || 0 })
1447
+
1448
+ return getUpdateNotification() + report
1449
+ }
1450
+ }),
1114
1451
  },
1115
1452
 
1116
1453
  // Event hooks
@@ -1126,6 +1463,77 @@ Use this at the end of the MAD workflow to ensure code quality.`,
1126
1463
  })
1127
1464
  }
1128
1465
  },
1466
+
1467
+ // Hook to enforce agent constraints before tool execution
1468
+ hook: {
1469
+ "tool.execute.before": async (input: any, output: any) => {
1470
+ const perms = agentPermissions.get(input.sessionID)
1471
+
1472
+ // If no permissions registered, let it pass (backwards compatibility)
1473
+ if (!perms) return
1474
+
1475
+ const toolName = input.tool
1476
+ const args = output.args || {}
1477
+
1478
+ // 1. Block edit/write/patch for read-only agents
1479
+ if (['edit', 'write', 'patch', 'multiedit'].includes(toolName)) {
1480
+ if (!perms.canEdit) {
1481
+ 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.`)
1483
+ }
1484
+
1485
+ // 2. Check path if allowedPaths is defined
1486
+ const targetPath = args.filePath || args.file_path || args.path
1487
+ if (targetPath) {
1488
+ // Check denied paths
1489
+ if (perms.deniedPaths.some((p: string) => targetPath.includes(p) || matchGlob(targetPath, p))) {
1490
+ 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.`)
1492
+ }
1493
+
1494
+ // Check allowed paths (if defined)
1495
+ if (perms.allowedPaths && perms.allowedPaths.length > 0) {
1496
+ const isAllowed = perms.allowedPaths.some((p: string) => targetPath.includes(p) || matchGlob(targetPath, p))
1497
+ 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(', ')}`)
1500
+ }
1501
+ }
1502
+ }
1503
+ }
1504
+
1505
+ // 3. For bash, check if trying to modify files
1506
+ if (toolName === 'bash' && perms && !perms.canEdit) {
1507
+ const cmd = args.command || ''
1508
+ 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
1519
+ ]
1520
+
1521
+ for (const pattern of dangerousPatterns) {
1522
+ 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.`)
1525
+ }
1526
+ }
1527
+ }
1528
+
1529
+ // 4. Force CWD in worktree for agents with worktree
1530
+ if (toolName === 'bash' && perms?.worktree && args.command) {
1531
+ // Prefix command with cd to worktree
1532
+ const worktreePath = perms.worktree.replace(/\\/g, '/')
1533
+ output.args.command = `cd "${worktreePath}" && ${args.command}`
1534
+ }
1535
+ }
1536
+ },
1129
1537
  }
1130
1538
  }
1131
1539