From fba83fd8223e7b429555ff250db658e783e9bfe5 Mon Sep 17 00:00:00 2001 From: zawakin Date: Sun, 8 Feb 2026 12:46:15 +0900 Subject: [PATCH 1/3] chore(skills): add singleton rule for git:open-pr to prevent duplicate watchers Co-Authored-By: Claude Opus 4.6 --- .claude/skills/git-workflow/SKILL.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.claude/skills/git-workflow/SKILL.md b/.claude/skills/git-workflow/SKILL.md index 387faf5..2ae1442 100644 --- a/.claude/skills/git-workflow/SKILL.md +++ b/.claude/skills/git-workflow/SKILL.md @@ -46,6 +46,7 @@ mise run git:sync # Sync current branch after base PR merge 4. Push -> git push -u origin feature/your-feature 5. PR -> gh pr create -a "@me" -t "feat: ..." 6. Open -> mise run git:open-pr (CI wait -> browser -> merge watch -> cleanup) + ⚠ Run ONCE after final push. If CI fails, stop watcher -> fix -> push -> relaunch. 7. Cleanup -> (auto: merge detected -> git:cleanup runs) ``` @@ -64,6 +65,12 @@ Claude: [Bash(run_in_background=true)] mise run git:open-pr -- - MERGED -> macOS notification + `mise run git:cleanup` -> exit - CLOSED -> message -> exit +**Singleton rule — only ONE watcher per PR:** +- `git:open-pr` must run **only once** per PR — after the final push, when no more changes are expected. +- If CI fails and you need to push a fix: **stop the existing watcher** with `TaskStop` first, fix and push, then launch a new `git:open-pr`. +- Never have multiple `git:open-pr` background tasks running for the same PR. +- Use `--no-wait` to skip CI wait phase when you just want the merge watcher. + **Claude behavior**: When background task output arrives via ``, Claude MUST: 1. Read the output file with `TaskOutput` or `Read` 2. Report the result to the user immediately From 00cda66acb15bc8ee67e7636960fdd9dcabc6c9e Mon Sep 17 00:00:00 2001 From: zawakin Date: Sun, 8 Feb 2026 12:49:27 +0900 Subject: [PATCH 2/3] chore: open PR in Chrome "PR" profile with fallback to default browser Co-Authored-By: Claude Opus 4.6 --- .claude/skills/git-workflow/SKILL.md | 2 +- tasks.toml | 26 ++++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/.claude/skills/git-workflow/SKILL.md b/.claude/skills/git-workflow/SKILL.md index 2ae1442..0a5340d 100644 --- a/.claude/skills/git-workflow/SKILL.md +++ b/.claude/skills/git-workflow/SKILL.md @@ -60,7 +60,7 @@ Claude: [Bash(run_in_background=true)] mise run git:open-pr -- 3 phases run automatically: 1. **CI wait** — `gh pr checks --watch` waits for CI to pass -2. **Open in browser** — Opens PR page in default browser +2. **Open in browser** — Chrome "PR" profile if available, else default browser 3. **Merge watch** — Polls PR state every 30s - MERGED -> macOS notification + `mise run git:cleanup` -> exit - CLOSED -> message -> exit diff --git a/tasks.toml b/tasks.toml index a961d76..8dac943 100644 --- a/tasks.toml +++ b/tasks.toml @@ -154,9 +154,31 @@ if [[ "$WAIT_CI" == true && -n "$PR_NUMBER" ]]; then gh pr checks "$PR_NUMBER" --watch || echo "[git:open-pr] CI checks failed or timed out, opening anyway" fi -# Phase 2: Open in browser +# Phase 2: Open in browser (Chrome "PR" profile if available, else default) echo "[git:open-pr] Phase 2/3: Opening PR in browser..." -gh pr view "$PR_NUMBER" --web +OPENED=false +if [[ "$(uname)" == "Darwin" ]]; then + LOCAL_STATE="$HOME/Library/Application Support/Google/Chrome/Local State" + if [[ -f "$LOCAL_STATE" ]]; then + PROFILE_DIR=$(python3 -c " +import json, sys, pathlib +data = json.loads((pathlib.Path.home() / 'Library/Application Support/Google/Chrome/Local State').read_text()) +profiles = data.get('profile', {}).get('info_cache', {}) +for key, val in profiles.items(): + if val.get('name') == 'PR': + print(key) + sys.exit(0) +sys.exit(1) +" 2>/dev/null) && { + open -na "Google Chrome" --args --profile-directory="$PROFILE_DIR" "$URL" + echo "Opened in Chrome profile \"PR\" ($PROFILE_DIR)" + OPENED=true + } + fi +fi +if [[ "$OPENED" == false ]]; then + gh pr view "$PR_NUMBER" --web +fi # Phase 3: Watch for merge if [[ -n "$PR_NUMBER" ]]; then From 48f0889bf85454302cbafca8e085dbd047620bc0 Mon Sep 17 00:00:00 2001 From: zawakin Date: Sun, 8 Feb 2026 12:51:44 +0900 Subject: [PATCH 3/3] chore: use local hook for PR browser open (gitignored, OSS-clean) Co-Authored-By: Claude Opus 4.6 --- .claude/skills/git-workflow/SKILL.md | 2 +- .gitignore | 3 +++ tasks.toml | 28 ++++++---------------------- 3 files changed, 10 insertions(+), 23 deletions(-) diff --git a/.claude/skills/git-workflow/SKILL.md b/.claude/skills/git-workflow/SKILL.md index 0a5340d..97aa80e 100644 --- a/.claude/skills/git-workflow/SKILL.md +++ b/.claude/skills/git-workflow/SKILL.md @@ -60,7 +60,7 @@ Claude: [Bash(run_in_background=true)] mise run git:open-pr -- 3 phases run automatically: 1. **CI wait** — `gh pr checks --watch` waits for CI to pass -2. **Open in browser** — Chrome "PR" profile if available, else default browser +2. **Open in browser** — Uses `scripts/open-url.local.sh` if present (gitignored), else default browser 3. **Merge watch** — Polls PR state every 30s - MERGED -> macOS notification + `mise run git:cleanup` -> exit - CLOSED -> message -> exit diff --git a/.gitignore b/.gitignore index da20dc3..ee7aeb7 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,9 @@ Cargo.lock .gemini/ .codex/ +# Local hooks (user-specific, not committed) +scripts/*.local.sh + /tmp/ OUTPUT.md crates/agtrace-sdk/examples/debug/ diff --git a/tasks.toml b/tasks.toml index 8dac943..fdc13f5 100644 --- a/tasks.toml +++ b/tasks.toml @@ -154,29 +154,13 @@ if [[ "$WAIT_CI" == true && -n "$PR_NUMBER" ]]; then gh pr checks "$PR_NUMBER" --watch || echo "[git:open-pr] CI checks failed or timed out, opening anyway" fi -# Phase 2: Open in browser (Chrome "PR" profile if available, else default) +# Phase 2: Open in browser +# Use local hook if available (gitignored), otherwise default browser echo "[git:open-pr] Phase 2/3: Opening PR in browser..." -OPENED=false -if [[ "$(uname)" == "Darwin" ]]; then - LOCAL_STATE="$HOME/Library/Application Support/Google/Chrome/Local State" - if [[ -f "$LOCAL_STATE" ]]; then - PROFILE_DIR=$(python3 -c " -import json, sys, pathlib -data = json.loads((pathlib.Path.home() / 'Library/Application Support/Google/Chrome/Local State').read_text()) -profiles = data.get('profile', {}).get('info_cache', {}) -for key, val in profiles.items(): - if val.get('name') == 'PR': - print(key) - sys.exit(0) -sys.exit(1) -" 2>/dev/null) && { - open -na "Google Chrome" --args --profile-directory="$PROFILE_DIR" "$URL" - echo "Opened in Chrome profile \"PR\" ($PROFILE_DIR)" - OPENED=true - } - fi -fi -if [[ "$OPENED" == false ]]; then +OPEN_HOOK="./scripts/open-url.local.sh" +if [[ -x "$OPEN_HOOK" ]]; then + "$OPEN_HOOK" "$URL" +else gh pr view "$PR_NUMBER" --web fi