diff --git a/plugins-claude/git-tools/.claude-plugin/plugin.json b/plugins-claude/git-tools/.claude-plugin/plugin.json index 518a5e1..50a3461 100644 --- a/plugins-claude/git-tools/.claude-plugin/plugin.json +++ b/plugins-claude/git-tools/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "git-tools", - "version": "2.2.1", + "version": "2.2.2", "description": "GitHub and Gitea tooling — unified CLI wrapper (issues, PRs, CI runs) plus a ship orchestrator that drives the full branch/commit/push/PR/watch/cleanup lifecycle", "author": { "name": "Logan Gagne" diff --git a/plugins-claude/git-tools/scripts/git-cli b/plugins-claude/git-tools/scripts/git-cli index 16ff305..2b232ce 100755 --- a/plugins-claude/git-tools/scripts/git-cli +++ b/plugins-claude/git-tools/scripts/git-cli @@ -137,14 +137,26 @@ parse_body_args() { COMMENT_JQ='{id, author: (.user.login // .user // ""), body, html_url: (.html_url // ""), created_at}' # Emit {number, url} JSON from a create command's human output by scraping the -# first issue/PR URL it printed. Used by issue/pr create so write commands return -# parseable JSON instead of gh/tea's human-rendered text. Dies with the raw -# output if no URL is found (so a silent format change surfaces loudly). +# create confirmation URL it printed. Used by issue/pr create so write commands +# return parseable JSON instead of gh/tea's human-rendered text. Dies with the +# raw output if no URL is found (so a silent format change surfaces loudly). +# +# tea renders the created object's body before its confirmation URL, so a body +# linking to another issue puts a decoy URL earlier in the output — hence the +# last URL-only line, and last (never first) in the fallback too. gh prints just +# the bare URL, so both paths are no-ops there. The fallback exists so an +# unrecognised output shape degrades to the old scrape instead of dying. emit_created_json() { local raw="$1" url num url=$(printf '%s\n' "$raw" \ - | grep -oiE 'https?://[^[:space:]]+/(issues?|pulls?)/[0-9]+' \ - | head -n1 || true) + | grep -oiE '^[[:space:]]*https?://[^[:space:]]+/(issues?|pulls?)/[0-9]+[[:space:]]*$' \ + | tail -n1 \ + | tr -d '[:space:]' || true) + if [[ -z "$url" ]]; then + url=$(printf '%s\n' "$raw" \ + | grep -oiE 'https?://[^[:space:]]+/(issues?|pulls?)/[0-9]+' \ + | tail -n1 || true) + fi [[ -n "$url" ]] || die "could not parse created object URL from output: $raw" num="${url##*/}" # The regex guarantees a trailing [0-9]+, so number is always a true integer. diff --git a/plugins-claude/session/.claude-plugin/plugin.json b/plugins-claude/session/.claude-plugin/plugin.json index 5c64db7..850b42c 100644 --- a/plugins-claude/session/.claude-plugin/plugin.json +++ b/plugins-claude/session/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "session", - "version": "4.5.0", + "version": "4.5.1", "description": "Work session management — issue-driven and freeform doors sharing an explore-then-plan spine, with multi-agent orchestration and a review-gated PR finalizer", "author": { "name": "Logan Gagne" diff --git a/plugins-claude/session/scripts/git-cli b/plugins-claude/session/scripts/git-cli index 16ff305..2b232ce 100755 --- a/plugins-claude/session/scripts/git-cli +++ b/plugins-claude/session/scripts/git-cli @@ -137,14 +137,26 @@ parse_body_args() { COMMENT_JQ='{id, author: (.user.login // .user // ""), body, html_url: (.html_url // ""), created_at}' # Emit {number, url} JSON from a create command's human output by scraping the -# first issue/PR URL it printed. Used by issue/pr create so write commands return -# parseable JSON instead of gh/tea's human-rendered text. Dies with the raw -# output if no URL is found (so a silent format change surfaces loudly). +# create confirmation URL it printed. Used by issue/pr create so write commands +# return parseable JSON instead of gh/tea's human-rendered text. Dies with the +# raw output if no URL is found (so a silent format change surfaces loudly). +# +# tea renders the created object's body before its confirmation URL, so a body +# linking to another issue puts a decoy URL earlier in the output — hence the +# last URL-only line, and last (never first) in the fallback too. gh prints just +# the bare URL, so both paths are no-ops there. The fallback exists so an +# unrecognised output shape degrades to the old scrape instead of dying. emit_created_json() { local raw="$1" url num url=$(printf '%s\n' "$raw" \ - | grep -oiE 'https?://[^[:space:]]+/(issues?|pulls?)/[0-9]+' \ - | head -n1 || true) + | grep -oiE '^[[:space:]]*https?://[^[:space:]]+/(issues?|pulls?)/[0-9]+[[:space:]]*$' \ + | tail -n1 \ + | tr -d '[:space:]' || true) + if [[ -z "$url" ]]; then + url=$(printf '%s\n' "$raw" \ + | grep -oiE 'https?://[^[:space:]]+/(issues?|pulls?)/[0-9]+' \ + | tail -n1 || true) + fi [[ -n "$url" ]] || die "could not parse created object URL from output: $raw" num="${url##*/}" # The regex guarantees a trailing [0-9]+, so number is always a true integer. diff --git a/plugins-copilot/git-tools/.claude-plugin/plugin.json b/plugins-copilot/git-tools/.claude-plugin/plugin.json index 518a5e1..50a3461 100644 --- a/plugins-copilot/git-tools/.claude-plugin/plugin.json +++ b/plugins-copilot/git-tools/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "git-tools", - "version": "2.2.1", + "version": "2.2.2", "description": "GitHub and Gitea tooling — unified CLI wrapper (issues, PRs, CI runs) plus a ship orchestrator that drives the full branch/commit/push/PR/watch/cleanup lifecycle", "author": { "name": "Logan Gagne" diff --git a/plugins-copilot/session/.claude-plugin/plugin.json b/plugins-copilot/session/.claude-plugin/plugin.json index 5c64db7..850b42c 100644 --- a/plugins-copilot/session/.claude-plugin/plugin.json +++ b/plugins-copilot/session/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "session", - "version": "4.5.0", + "version": "4.5.1", "description": "Work session management — issue-driven and freeform doors sharing an explore-then-plan spine, with multi-agent orchestration and a review-gated PR finalizer", "author": { "name": "Logan Gagne" diff --git a/tests/git-cli/test-issue-write-json.sh b/tests/git-cli/test-issue-write-json.sh index 37a89d8..bc437d6 100644 --- a/tests/git-cli/test-issue-write-json.sh +++ b/tests/git-cli/test-issue-write-json.sh @@ -83,8 +83,26 @@ case "$1 $2 $3" in "login list --output") echo '[{"url":"https://git.stonefish.tech","user":"alice"}]'; exit 0 ;; esac case "$1 $2" in - "issues create") echo "Created issue https://git.stonefish.tech/owner/repo/issues/42"; exit 0 ;; - "pr create") echo "Created pull https://git.stonefish.tech/owner/repo/pulls/7"; exit 0 ;; + # Realistic tea output, not a synthetic one-liner: tea renders the whole + # created object (title/author/body, indented) before a flush-left + # confirmation URL. Decoy #1 (both cases): a markdown link to issue #1 + # embedded mid-body, mirroring how real tracker tickets link back to a + # parent issue — the anchored regex alone rejects this (it's not a bare + # URL line), so it also guards the pre-anchor fallback path. Decoy #2 + # (pr create): a *bare* URL on its own indented line, e.g. a "See " + # reference — this one DOES match the anchored "line is just a URL" + # pattern, so only taking the *last* match (not first) tells it apart + # from the true confirmation line that follows. Together these catch a + # regression of the bug where emit_created_json grabbed the first + # issue/pull URL anywhere in the output instead of the trailing + # confirmation line, always reporting the linked issue's number (#1) + # instead of the one actually created (#42 / #7). + "issues create") + printf ' # #42 x (open)\n\n @alice created 2026-06-02\n\n Child of [parent](https://git.stonefish.tech/owner/repo/issues/1).\n\nhttps://git.stonefish.tech/owner/repo/issues/42\n' + exit 0 ;; + "pr create") + printf ' # #7 x (open)\n\n @alice wants to merge\n\n Fixes [parent](https://git.stonefish.tech/owner/repo/issues/1).\n\n See also\n https://git.stonefish.tech/owner/repo/issues/1\n\nhttps://git.stonefish.tech/owner/repo/pulls/7\n' + exit 0 ;; esac if [[ "$1" == "api" ]]; then case "$args" in diff --git a/utils/git-cli b/utils/git-cli index 16ff305..2b232ce 100755 --- a/utils/git-cli +++ b/utils/git-cli @@ -137,14 +137,26 @@ parse_body_args() { COMMENT_JQ='{id, author: (.user.login // .user // ""), body, html_url: (.html_url // ""), created_at}' # Emit {number, url} JSON from a create command's human output by scraping the -# first issue/PR URL it printed. Used by issue/pr create so write commands return -# parseable JSON instead of gh/tea's human-rendered text. Dies with the raw -# output if no URL is found (so a silent format change surfaces loudly). +# create confirmation URL it printed. Used by issue/pr create so write commands +# return parseable JSON instead of gh/tea's human-rendered text. Dies with the +# raw output if no URL is found (so a silent format change surfaces loudly). +# +# tea renders the created object's body before its confirmation URL, so a body +# linking to another issue puts a decoy URL earlier in the output — hence the +# last URL-only line, and last (never first) in the fallback too. gh prints just +# the bare URL, so both paths are no-ops there. The fallback exists so an +# unrecognised output shape degrades to the old scrape instead of dying. emit_created_json() { local raw="$1" url num url=$(printf '%s\n' "$raw" \ - | grep -oiE 'https?://[^[:space:]]+/(issues?|pulls?)/[0-9]+' \ - | head -n1 || true) + | grep -oiE '^[[:space:]]*https?://[^[:space:]]+/(issues?|pulls?)/[0-9]+[[:space:]]*$' \ + | tail -n1 \ + | tr -d '[:space:]' || true) + if [[ -z "$url" ]]; then + url=$(printf '%s\n' "$raw" \ + | grep -oiE 'https?://[^[:space:]]+/(issues?|pulls?)/[0-9]+' \ + | tail -n1 || true) + fi [[ -n "$url" ]] || die "could not parse created object URL from output: $raw" num="${url##*/}" # The regex guarantees a trailing [0-9]+, so number is always a true integer.