diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index b6b2184..b16ae20 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,7 +1,7 @@ { "$schema": "https://anthropic.com/claude-code/marketplace.schema.json", "name": "make-no-mistakes", - "version": "1.10.0", + "version": "1.11.0", "description": "The disciplined dev lifecycle — implement issues, review PRs, sync releases, test E2E, manage sessions, and stash secrets via OS-native prompts. One plugin to make no mistakes.", "owner": { "name": "Luis Andres Pena Castillo", @@ -11,7 +11,7 @@ { "name": "make-no-mistakes", "description": "Dev lifecycle orchestrator: disciplined Linear issue execution with worktree isolation, PR review with Greptile gating, team release sync, E2E test generation and execution, test suite previewer, security pentesting, MoSCoW + RICE prioritization, cross-platform secret stash via OS-native GUI prompts (zenity / kdialog / osascript / Get-Credential), and session management. 18 commands, 6 auto-activating skills, 2 specialized agents.", - "version": "1.10.0", + "version": "1.11.0", "author": { "name": "Luis Andres Pena Castillo", "email": "lapc506@users.noreply.github.com" diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index d84a645..4372ccb 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "make-no-mistakes", - "version": "1.10.0", + "version": "1.11.0", "description": "The disciplined dev lifecycle — implement issues, review PRs, sync releases, test E2E, manage sessions, stash secrets, and enforce manifest-driven tool-call hooks. One plugin to make no mistakes.", "author": { "name": "Luis Andres Pena Castillo", diff --git a/hooks/rules/rules.json b/hooks/rules/rules.json index 3c5f3e1..a7780da 100644 --- a/hooks/rules/rules.json +++ b/hooks/rules/rules.json @@ -2402,5 +2402,241 @@ "expected_exit": 0 } ] + }, + { + "id": "warn-bash-mutation-without-leading-cd", + "description": "Warn when bash mutation commands (git commit/push/mv/rm/reset/merge/revert, sed -i / sed --in-place, gh pr create/merge/edit) don't start with an absolute-path `cd /<...>` (or `pushd /<...>`, `cd ~/<...>`, `cd $VAR/<...>`) — prevents commits landing on the wrong branch when working across multiple parallel worktrees", + "applies_to": [ + "Bash" + ], + "match": [ + { + "field": "command", + "pattern": "git[[:space:]]+(commit|push|mv|rm[[:space:]]|reset|revert|merge)|sed[[:space:]]+-i([[:space:]]|[^[:space:]-])|sed[[:space:]]+--in-place[[:space:]]|gh[[:space:]]+pr[[:space:]]+(create|merge|edit)" + }, + { + "field": "command", + "not_pattern": "^[[:space:]]*(cd|pushd)[[:space:]]+(/|~|\\$)" + } + ], + "action": "warn", + "bypass_marker": "cd-worktree-rule", + "memory_ref": "feedback_cd_between_worktrees.md", + "references": [ + "DOJ-4007 PR-2/PR-3 cross-ref fixup (2026-05-10) — bug that motivated this rule" + ], + "message": "WARN: bash mutation command (git commit/push/mv/rm/reset/merge/revert,\nsed -i, or gh pr create/merge/edit) detected without a leading\n`cd /` prefix.\n\nPer feedback_cd_between_worktrees.md: when working across multiple\nparallel git worktrees in the same session, EVERY bash invocation that\ntouches a specific worktree MUST start with explicit\n`cd /full/path/to/worktree && ...`.\n\nBug pattern this prevents (real, 2026-05-10):\n- Two parallel worktrees for two PRs (PR-2 + PR-3 of same repo).\n- First worktree's sed + commit + push: correct (cwd was PR-2).\n- Second worktree's sed + commit + push: missing cd → still in PR-2 cwd\n → commit landed on PR-2 branch instead of PR-3 branch.\n- Required revert on PR-2 + correct re-apply on PR-3. Confused commit\n history visible to reviewers.\n\nIf the command operates on absolute paths or doesn't need cwd context,\nor you've intentionally verified cwd via `pwd` immediately before,\nadd \"# hook-bypass: cd-worktree-rule\" to acknowledge.\n", + "tests": [ + { + "name": "warns-on-git-commit-no-cd", + "input": { + "tool_input": { + "command": "git add foo.md && git commit -m \"fix\"" + } + }, + "expected_exit": 0, + "expected_stderr_contains": "warn-bash-mutation-without-leading-cd" + }, + { + "name": "warns-on-git-push-no-cd", + "input": { + "tool_input": { + "command": "git push origin main" + } + }, + "expected_exit": 0, + "expected_stderr_contains": "warn-bash-mutation-without-leading-cd" + }, + { + "name": "warns-on-git-mv-no-cd", + "input": { + "tool_input": { + "command": "git mv content/old content/new" + } + }, + "expected_exit": 0, + "expected_stderr_contains": "warn-bash-mutation-without-leading-cd" + }, + { + "name": "warns-on-git-rm-no-cd", + "input": { + "tool_input": { + "command": "git rm path/to/file.md" + } + }, + "expected_exit": 0, + "expected_stderr_contains": "warn-bash-mutation-without-leading-cd" + }, + { + "name": "warns-on-sed-i-no-cd", + "input": { + "tool_input": { + "command": "sed -i s/foo/bar/g content/file.md" + } + }, + "expected_exit": 0, + "expected_stderr_contains": "warn-bash-mutation-without-leading-cd" + }, + { + "name": "warns-on-sed-i-bak-no-cd", + "input": { + "tool_input": { + "command": "sed -i.bak s/foo/bar/g content/file.md" + } + }, + "expected_exit": 0, + "expected_stderr_contains": "warn-bash-mutation-without-leading-cd" + }, + { + "name": "warns-on-sed-in-place-long-form-no-cd", + "input": { + "tool_input": { + "command": "sed --in-place s/foo/bar/g content/file.md" + } + }, + "expected_exit": 0, + "expected_stderr_contains": "warn-bash-mutation-without-leading-cd" + }, + { + "name": "warns-on-git-commit-with-relative-cd", + "input": { + "tool_input": { + "command": "cd worktree-dir && git commit -m \"fix\"" + } + }, + "expected_exit": 0, + "expected_stderr_contains": "warn-bash-mutation-without-leading-cd" + }, + { + "name": "warns-on-git-push-with-dotdot-cd", + "input": { + "tool_input": { + "command": "cd ../sibling-dir && git push" + } + }, + "expected_exit": 0, + "expected_stderr_contains": "warn-bash-mutation-without-leading-cd" + }, + { + "name": "warns-on-gh-pr-create-no-cd", + "input": { + "tool_input": { + "command": "gh pr create --base main --title foo" + } + }, + "expected_exit": 0, + "expected_stderr_contains": "warn-bash-mutation-without-leading-cd" + }, + { + "name": "warns-on-gh-pr-merge-no-cd", + "input": { + "tool_input": { + "command": "gh pr merge 27 --squash --delete-branch" + } + }, + "expected_exit": 0, + "expected_stderr_contains": "warn-bash-mutation-without-leading-cd" + }, + { + "name": "warns-on-git-reset-hard-no-cd", + "input": { + "tool_input": { + "command": "git reset --hard origin/main" + } + }, + "expected_exit": 0, + "expected_stderr_contains": "warn-bash-mutation-without-leading-cd" + }, + { + "name": "passes-with-cd-prefix", + "input": { + "tool_input": { + "command": "cd /home/user/repo && git commit -m fix && git push" + } + }, + "expected_exit": 0 + }, + { + "name": "passes-with-pushd-prefix", + "input": { + "tool_input": { + "command": "pushd /home/user/repo && git commit -m fix" + } + }, + "expected_exit": 0 + }, + { + "name": "passes-with-tilde-cd-prefix", + "input": { + "tool_input": { + "command": "cd ~/repo && git commit -m fix" + } + }, + "expected_exit": 0 + }, + { + "name": "passes-with-env-var-cd-prefix", + "input": { + "tool_input": { + "command": "cd $WORKTREE && git push" + } + }, + "expected_exit": 0 + }, + { + "name": "passes-on-git-status", + "input": { + "tool_input": { + "command": "git status" + } + }, + "expected_exit": 0 + }, + { + "name": "passes-on-git-log", + "input": { + "tool_input": { + "command": "git log --oneline -3" + } + }, + "expected_exit": 0 + }, + { + "name": "passes-on-git-diff", + "input": { + "tool_input": { + "command": "git diff --stat" + } + }, + "expected_exit": 0 + }, + { + "name": "passes-on-gh-pr-list", + "input": { + "tool_input": { + "command": "gh pr list --author=@me" + } + }, + "expected_exit": 0 + }, + { + "name": "passes-on-gh-pr-view", + "input": { + "tool_input": { + "command": "gh pr view 27 --json mergeable" + } + }, + "expected_exit": 0 + }, + { + "name": "passes-with-bypass-marker", + "input": { + "tool_input": { + "command": "git push origin main # hook-bypass: cd-worktree-rule" + } + }, + "expected_exit": 0 + } + ] } ] diff --git a/hooks/rules/rules.yaml b/hooks/rules/rules.yaml index b5f622e..b90e75f 100644 --- a/hooks/rules/rules.yaml +++ b/hooks/rules/rules.yaml @@ -1919,3 +1919,167 @@ tool_input: command: 'gh pr create --body "Run bun run dev on localhost:5173 # hook-bypass: localhost-in-pr-body-allowed"' expected_exit: 0 + +- id: warn-bash-mutation-without-leading-cd + description: Warn when bash mutation commands (git commit/push/mv/rm/reset/merge/revert, sed -i / sed --in-place, gh pr create/merge/edit) don't start with an absolute-path `cd /<...>` (or `pushd /<...>`, `cd ~/<...>`, `cd $VAR/<...>`) — prevents commits landing on the wrong branch when working across multiple parallel worktrees + applies_to: [Bash] + match: + # Positive: command contains a state-mutating op. + # sed pattern covers: `sed -i `, `sed -i.bak`, `sed -ibackup`, and long form `sed --in-place`. + - field: command + pattern: 'git[[:space:]]+(commit|push|mv|rm[[:space:]]|reset|revert|merge)|sed[[:space:]]+-i([[:space:]]|[^[:space:]-])|sed[[:space:]]+--in-place[[:space:]]|gh[[:space:]]+pr[[:space:]]+(create|merge|edit)' + # Negative: command must START with `cd ` or `pushd `. + # Accepts: cd /full/path, pushd /full/path, cd ~/path, cd $VAR/path (env vars assumed absolute). + # Rejects: cd worktree-dir (relative path — DOESN'T fix the cwd-confusion bug because if + # cwd is already in the wrong worktree, `cd relative-dir` resolves to the wrong place). + - field: command + not_pattern: '^[[:space:]]*(cd|pushd)[[:space:]]+(/|~|\$)' + action: warn + bypass_marker: cd-worktree-rule + memory_ref: feedback_cd_between_worktrees.md + references: + - "DOJ-4007 PR-2/PR-3 cross-ref fixup (2026-05-10) — bug that motivated this rule" + message: | + WARN: bash mutation command (git commit/push/mv/rm/reset/merge/revert, + sed -i, or gh pr create/merge/edit) detected without a leading + `cd /` prefix. + + Per feedback_cd_between_worktrees.md: when working across multiple + parallel git worktrees in the same session, EVERY bash invocation that + touches a specific worktree MUST start with explicit + `cd /full/path/to/worktree && ...`. + + Bug pattern this prevents (real, 2026-05-10): + - Two parallel worktrees for two PRs (PR-2 + PR-3 of same repo). + - First worktree's sed + commit + push: correct (cwd was PR-2). + - Second worktree's sed + commit + push: missing cd → still in PR-2 cwd + → commit landed on PR-2 branch instead of PR-3 branch. + - Required revert on PR-2 + correct re-apply on PR-3. Confused commit + history visible to reviewers. + + If the command operates on absolute paths or doesn't need cwd context, + or you've intentionally verified cwd via `pwd` immediately before, + add "# hook-bypass: cd-worktree-rule" to acknowledge. + tests: + - name: warns-on-git-commit-no-cd + input: + tool_input: + command: 'git add foo.md && git commit -m "fix"' + expected_exit: 0 + expected_stderr_contains: 'warn-bash-mutation-without-leading-cd' + - name: warns-on-git-push-no-cd + input: + tool_input: + command: 'git push origin main' + expected_exit: 0 + expected_stderr_contains: 'warn-bash-mutation-without-leading-cd' + - name: warns-on-git-mv-no-cd + input: + tool_input: + command: 'git mv content/old content/new' + expected_exit: 0 + expected_stderr_contains: 'warn-bash-mutation-without-leading-cd' + - name: warns-on-git-rm-no-cd + input: + tool_input: + command: 'git rm path/to/file.md' + expected_exit: 0 + expected_stderr_contains: 'warn-bash-mutation-without-leading-cd' + - name: warns-on-sed-i-no-cd + input: + tool_input: + command: 'sed -i s/foo/bar/g content/file.md' + expected_exit: 0 + expected_stderr_contains: 'warn-bash-mutation-without-leading-cd' + - name: warns-on-sed-i-bak-no-cd + input: + tool_input: + command: 'sed -i.bak s/foo/bar/g content/file.md' + expected_exit: 0 + expected_stderr_contains: 'warn-bash-mutation-without-leading-cd' + - name: warns-on-sed-in-place-long-form-no-cd + input: + tool_input: + command: 'sed --in-place s/foo/bar/g content/file.md' + expected_exit: 0 + expected_stderr_contains: 'warn-bash-mutation-without-leading-cd' + - name: warns-on-git-commit-with-relative-cd + input: + tool_input: + command: 'cd worktree-dir && git commit -m "fix"' + expected_exit: 0 + expected_stderr_contains: 'warn-bash-mutation-without-leading-cd' + - name: warns-on-git-push-with-dotdot-cd + input: + tool_input: + command: 'cd ../sibling-dir && git push' + expected_exit: 0 + expected_stderr_contains: 'warn-bash-mutation-without-leading-cd' + - name: warns-on-gh-pr-create-no-cd + input: + tool_input: + command: 'gh pr create --base main --title foo' + expected_exit: 0 + expected_stderr_contains: 'warn-bash-mutation-without-leading-cd' + - name: warns-on-gh-pr-merge-no-cd + input: + tool_input: + command: 'gh pr merge 27 --squash --delete-branch' + expected_exit: 0 + expected_stderr_contains: 'warn-bash-mutation-without-leading-cd' + - name: warns-on-git-reset-hard-no-cd + input: + tool_input: + command: 'git reset --hard origin/main' + expected_exit: 0 + expected_stderr_contains: 'warn-bash-mutation-without-leading-cd' + - name: passes-with-cd-prefix + input: + tool_input: + command: 'cd /home/user/repo && git commit -m fix && git push' + expected_exit: 0 + - name: passes-with-pushd-prefix + input: + tool_input: + command: 'pushd /home/user/repo && git commit -m fix' + expected_exit: 0 + - name: passes-with-tilde-cd-prefix + input: + tool_input: + command: 'cd ~/repo && git commit -m fix' + expected_exit: 0 + - name: passes-with-env-var-cd-prefix + input: + tool_input: + command: 'cd $WORKTREE && git push' + expected_exit: 0 + - name: passes-on-git-status + input: + tool_input: + command: 'git status' + expected_exit: 0 + - name: passes-on-git-log + input: + tool_input: + command: 'git log --oneline -3' + expected_exit: 0 + - name: passes-on-git-diff + input: + tool_input: + command: 'git diff --stat' + expected_exit: 0 + - name: passes-on-gh-pr-list + input: + tool_input: + command: 'gh pr list --author=@me' + expected_exit: 0 + - name: passes-on-gh-pr-view + input: + tool_input: + command: 'gh pr view 27 --json mergeable' + expected_exit: 0 + - name: passes-with-bypass-marker + input: + tool_input: + command: 'git push origin main # hook-bypass: cd-worktree-rule' + expected_exit: 0 diff --git a/package.json b/package.json index 555c610..f43955e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lapc506/make-no-mistakes", - "version": "1.10.0", + "version": "1.11.0", "description": "The disciplined dev lifecycle — implement issues, review PRs, sync releases, test E2E, manage sessions, stash secrets, and enforce manifest-driven tool-call hooks (no SSH+DB, no manual prod, no minified build, no secret leaks, Slack format). OpenCode + Claude Code plugin.", "type": "module", "main": "./dist/index.js",