Skip to content
Back to skills

Issue Close

ASecurity

イシュー完了時に使用。PRマージ・worktree削除・ブランチ安全削除を一括実行

  • 15 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 1, 2026
ai-agentsbashgit

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned October 1, 2026

npx -y skills add apokamo/kaji --skill issue-close --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Issue Close?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Issue Close
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/apokamo-issue-close/badge)](https://www.skillsdirectory.com/skills/apokamo-issue-close)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
description: イシュー完了時に使用。PRマージ・worktree削除・ブランチ安全削除を一括実行
name: issue-close
---

# Issue Close

イシュー対応完了後のクリーンアップを実行します。
PR マージ、worktree 削除、ブランチ削除、Issue クローズを一括実行します。

## いつ使うか

| タイミング | このスキルを使用 |
|-----------|-----------------|
| PRがApproveされマージ可能 | ✅ 使用 |
| PRレビュー待ち | ❌ 待機 |
| 作業途中 | ❌ 不要 |

**ワークフロー内の位置**: implement → review-code → i-dev-final-check → i-pr → **close**

## 入力

### ハーネス経由(コンテキスト変数)

**常に注入される変数:**

| 変数 | 型 | 説明 |
|------|-----|------|
| `issue_id` | str | 正規化済み Issue ID(GitHub 数値または local ID) |
| `issue_ref` | str | 人間可読の Issue 参照(GitHub では `#<issue_id>`、local では bare ID) |
| `step_id` | str | 現在のステップ ID |

**provider 解決時に追加で注入される変数:**

| 変数 | 型 | 説明 |
|------|-----|------|
| `provider_type` | str | `github` / `local` のいずれか。本 Skill の経路分岐に使用 |
| `default_branch` | str | ベースブランチ名(`main` 等)。local 経路で merge / push の引数に使用 |
| `branch_name` | str | フィーチャーブランチ名(`feat/<id>` 等) |
| `worktree_dir` | str | worktree 絶対パス |

### 手動実行(スラッシュコマンド)

```
$ARGUMENTS = <issue_id>
```

### 解決ルール

コンテキスト変数 `issue_id` が存在すればそちらを使用。
なければ `$ARGUMENTS` の第1引数を `issue_id` として使用。

`issue_ref` はハーネス経由ではプロンプトに自動注入される(`prompt.py` 側で provider 別に整形)。手動実行時は `issue_id` から導出する: GitHub 数値 ID なら `#<issue_id>`、`local-*` 形式なら bare ID(`#` を付けない)。

## 前提条件

- `/i-pr` でPRが作成済みであること
- Merge commit方式を使用(ブランチ履歴を保持)

## 実行手順

`[provider_type]` に応じて手順が分岐する。

- `[provider_type]` が `github`(または未注入の legacy 環境)→ 既存の
  Step 1〜6 を順に実行する(`kaji pr merge` / worktree 削除 / branch 削除 /
  `git pull` / `kaji issue close` / 報告)。
- `[provider_type]` が `local` → 後述の **provider=local の場合** セクションへ
  ジャンプし、design.md § local mode における /issue-close の手順 (6 step) を
  実行する。github 用の Step 1〜6 は実行しない(PR 概念が無いため)。

## 共通: ワークフロー完了後の確認項目の移管

親 Issue を close する直前に、provider に関係なく本手順を実行する。
正本は
[`docs/dev/workflow_completion_criteria.md`](../../../docs/dev/workflow_completion_criteria.md)
§ follow-up Issue への移管、本文 template は
[`templates/follow-up-issue.md`](templates/follow-up-issue.md) とする。

### 1. 未完了項目の抽出

親本文の `## 完了条件` 内にある末尾サブセクション
`### ワークフロー完了後の確認項目` だけを対象にし、次の `## ` 見出しまたは本文末尾までから
`- [ ]` の項目を抽出する。

| 状態 | 処理 |
|------|------|
| セクションなし | follow-up なしで close 手順を続行 |
| `- なし`、または未チェック項目 0 件 | follow-up なしで close 手順を続行 |
| 未チェック項目 1 件以上 | 以降の検索・作成・マーカー追記を実行 |
| 見出し重複、チェックリストとして解釈不能 | 項目喪失を避けるため `ABORT` |

`i-dev-final-check` / `i-doc-final-check` が同サブセクションを更新対象外としているため、
ここに残る `[ ]` は未完了のまま follow-up へ移す。`[x]` は移さない。

### 2. 親マーカーと既存 Issue の確認

follow-up のタイトルは次の完全一致形式に固定する。

```text
[follow-up] [parent_title] ([issue_ref])
```

まず親本文に次のマーカーがあるか確認する。

```text
<!-- kaji-follow-up-issue: [follow_up_issue_id] -->
```

- マーカーが 1 件ある場合: 値を `FOLLOW_UP_ID` に設定して `kaji issue view` で確認し、
  次の 3 点をすべて満たすときだけ再利用する
  1. `state` が `open` である
  2. title が完全一致形式である
  3. 本文に `<!-- kaji-follow-up-parent: [issue_ref] -->` が 1 件ある
- マーカーが複数、値が空、参照先が存在しない場合: `ABORT`
- マーカーの参照先が close 済みの場合: `ABORT`。未チェック項目が親に残っているのに
  追跡先が閉じている状態であり、close 済み follow-up を再利用すると追跡が失われる。
  同名タイトルの open Issue 検索へフォールバックしてもマーカーとの不整合が残るため、
  自動復旧はしない。人間が (a) 完了済みの項目を親本文で `[x]` に更新する、
  (b) follow-up Issue を reopen する、(c) 親のマーカー行を削除して再作成させる、
  のいずれかを選んだうえで再実行する
- マーカーがない場合: open Issue を完全一致タイトルで検索する

```bash
FOLLOW_UP_STATE=$(kaji issue view "$FOLLOW_UP_ID" --json state -q '.state')
# GitHub は OPEN/CLOSED、local は open/closed を返すため小文字化して比較する
case "$(printf '%s' "$FOLLOW_UP_STATE" | tr '[:upper:]' '[:lower:]')" in
  open) : ;;
  *)
    echo "ABORT: marked follow-up Issue $FOLLOW_UP_ID is not open (state=$FOLLOW_UP_STATE)"
    exit 1
    ;;
esac
```

```bash
PARENT_TITLE=$(kaji issue view [issue_id] --json title -q '.title')
FOLLOW_UP_TITLE="[follow-up] $PARENT_TITLE ([issue_ref])"
kaji issue list --state open --limit 1000 --json number,title > /tmp/kaji-open-issues.json
jq -r --arg title "$FOLLOW_UP_TITLE" \
  '.[] | select(.title == $title) | .number' \
  /tmp/kaji-open-issues.json > /tmp/kaji-follow-up-candidates.txt
```

| 完全一致件数 | 処理 |
|-------------|------|
| 0 | 新規作成 |
| 1 | 候補の `.number` を `FOLLOW_UP_ID` に設定し、既存 open Issue を再利用 |
| 2 以上 | 対象を一意に決められないため `ABORT` |

作成後にマーカー追記だけが失敗した場合も、再実行時は完全一致検索で作成済み Issue を
再利用する。これにより重複起票を防ぐ。

### 3. follow-up Issue の作成

完全一致候補がない場合だけ、`templates/follow-up-issue.md` を読み、以下を実値へ置換して
`/tmp/kaji-follow-up-body.md` を作る。

- `[parent_issue_ref]` / `[parent_issue_title]`
- `[unchecked_items]`: 抽出した未チェック項目だけ
- `[reference_docs]`: 親本文の `docs/...` 参照。なければ正本
  `docs/dev/workflow_completion_criteria.md`

```bash
CREATE_OUTPUT=$(kaji issue create \
  --title "$FOLLOW_UP_TITLE" \
  --body-file /tmp/kaji-follow-up-body.md) || {
    echo "ABORT: follow-up Issue creation failed"
    exit 1
  }
FOLLOW_UP_ID=${CREATE_OUTPUT##*/}

# local provider では create した issue.md を atomic commit する。
# github provider では --commit が除去されるため同じコマンドを使える。
kaji issue edit "$FOLLOW_UP_ID" --commit \
  --body-file /tmp/kaji-follow-up-body.md || {
    echo "ABORT: follow-up Issue persistence failed"
    exit 1
  }
```

### 4. 親本文へのマーカー追記

親本文に有効なマーカーがなかった場合だけ、新規作成・既存検索で得た Issue ID の
マーカーを親本文末尾へ 1 件追記する。Step 2 で有効なマーカーを確認済みの場合は追記を
スキップし、既存の 1 件を維持する。

```bash
CURRENT_BODY=$(kaji issue view [issue_id] --json body -q '.body')
{
  printf '%s\n\n' "$CURRENT_BODY"
  printf '<!-- kaji-follow-up-issue: %s -->\n' "$FOLLOW_UP_ID"
} > /tmp/kaji-parent-body-with-follow-up.md

kaji issue edit [issue_id] --commit \
  --body-file /tmp/kaji-parent-body-with-follow-up.md || {
    echo "ABORT: parent follow-up marker update failed; parent Issue remains open"
    exit 1
  }
```

追記後、`kaji issue view` で同じマーカーが 1 件だけ存在することを確認する。作成・再利用、
マーカー追記、追記後確認のいずれかが失敗した場合は `ABORT` とし、親 Issue の close を
実行しない。

> **結果を記録**: `follow_up_result` = 「対象なし」/「作成: [follow_up_issue_id]」/
> 「既存再利用: [follow_up_issue_id]」。完了報告に含める。

### provider=github の場合

### Step 1: Worktree情報の取得

Issue本文からWorktree情報を取得します:

```bash
kaji issue view [issue_id] --json body -q '.body'
```

以下の情報を抽出:
- `> **Worktree**: \`../kaji-[prefix]-[issue_id]\`` → worktree パス
- `> **Branch**: \`[prefix]/[issue_id]\`` → ブランチ名

### Step 2: メインリポジトリのパスを特定

`git worktree list` の最初の行が常に main worktree(bare repository のルート)を示す:

```bash
MAIN_REPO=$(git worktree list | head -1 | awk '{print $1}')
```

> **注意**: `git rev-parse --show-toplevel` は現在の worktree のルートを返すため、
> worktree 内から実行すると main repo を取得できない。必ず `git worktree list` を使うこと。

worktree 内にいる場合は main repo に移動:

```bash
cd "$MAIN_REPO"
```

### Step 3: PRのマージ

```bash
kaji pr merge [branch_name]
```

マージコミットを作成してブランチ履歴を保持する。ブランチ削除は worktree 削除後に Step 4.5 で行う。

> **結果を記録**: `pr_merge_result` = 「マージ済み」。この値は Step 6 で使用する。

### Step 4: worktree削除

```bash
git worktree remove "$MAIN_REPO/../kaji-[prefix]-[issue_id]"
```

> `$MAIN_REPO` は Step 2 で取得済み。

> **結果を記録**: `worktree_result` = 「削除済み」。この値は Step 6 で使用する。

### Step 4.5: ブランチ削除

worktree 削除後にローカル・リモートブランチを削除する。
`git fetch [git_remote]` で `[git_remote]/[default_branch]` を最新化してから、`merge-base --is-ancestor` でマージ済み判定を行い、安全に削除する。
ローカル削除とリモート削除は独立して実行し、片方の失敗がもう片方をブロックしない。
ブランチが既に存在しない場合はスキップする。

```bash
# 1. fetch して [git_remote]/[default_branch] を更新
git fetch [git_remote]

# 2. ローカルブランチ削除: 存在確認 → マージ済み判定 → 安全な -D
if git show-ref --verify --quiet refs/heads/[branch_name]; then
    if git merge-base --is-ancestor [branch_name] [git_remote]/[default_branch]; then
        git branch -D [branch_name]
    else
        echo "WARNING: branch not merged into [git_remote]/[default_branch], skipping local delete"
    fi
fi

# 3. リモートブランチ削除(ローカル削除の成否に依存しない)
git ls-remote --exit-code --heads [git_remote] [branch_name] >/dev/null 2>&1
LS_EXIT=$?
if [ "$LS_EXIT" -eq 0 ]; then
    if ! git push [git_remote] --delete [branch_name]; then
        echo "ERROR: git push [git_remote] --delete failed"
        exit 1
    fi
elif [ "$LS_EXIT" -eq 2 ]; then
    echo "INFO: remote branch already deleted"
else
    echo "ERROR: git ls-remote failed (exit $LS_EXIT)"
    exit 1
fi

# 4. stale remote-tracking ref を掃除
git fetch --prune [git_remote]
```

> **結果を記録**:
> - `local_branch_result` = 「削除済み」/「未存在を確認」/「未マージのためスキップ」
> - `remote_branch_result` = 「削除済み」/「未存在を確認」/「削除失敗(要手動対応)」
>
> これらの値は Step 6 で使用する。

### Step 5: mainを最新化

```bash
git pull [git_remote] [default_branch]
```

> **結果を記録**: `pull_result` = 「最新化済み」。この値は Step 6 で使用する。

### Step 5.4: ワークフロー完了後の確認項目を移管

「共通: ワークフロー完了後の確認項目の移管」を実行する。失敗時は `ABORT` とし、
Step 5.5 へ進まない。

### Step 5.5: Issue クローズ

```bash
kaji issue close [issue_id] --reason completed
```

> **結果を記録**: `close_result` = 「クローズ済み」/「クローズ失敗(要手動対応)」。この値は Step 6 で使用する。
>
> **重要**: `kaji issue close` が失敗した場合は verdict を **ABORT** にすること。Issue が未クローズのまま残ることは許容しない。

### Step 6: 完了報告

Step 3〜5.5 の結果を使って、**stdout への報告**と **Issue タイムラインへのコメント投稿**の両方を行う。

#### 6a. Issue コメント投稿

各ステップで記録した結果変数を使い、コメント内容を動的に組み立てて投稿する:

```bash
kaji issue comment [issue_id] --commit --body-file - <<'COMMENT_EOF'
## Issue クローズ完了

| 項目 | 状態 |
|------|------|
| PR | [pr_merge_result] |
| worktree | [worktree_result] |
| ローカルブランチ | [local_branch_result] |
| リモートブランチ | [remote_branch_result] |
| main | [pull_result] |
| follow-up Issue | [follow_up_result] |
| Issue | [close_result] |
COMMENT_EOF

```

> `[pr_merge_result]` 等のプレースホルダーは、実際の実行結果に置き換えること。ハードコードしない。

#### 6b. stdout 報告

以下の形式で報告してください:

```
## Issue クローズ完了

| 項目 | 状態 |
|------|------|
| Issue | [issue_ref] |
| PR | [pr_merge_result] |
| worktree | [worktree_result] |
| ローカルブランチ | [local_branch_result] |
| リモートブランチ | [remote_branch_result] |
| main | [pull_result] |
| follow-up Issue | [follow_up_result] |
| Issue 状態 | [close_result] |
```

### provider=local の場合

`[provider_type]` が `local` のとき、PR 概念が無いため
design.md § local mode における /issue-close の手順 (6 step) を実行する。

> **重要 (worktree 運用)**: bare repository + worktree パターンでは
> `[default_branch]` は **feature worktree とは別の worktree**(通常 main repo 側)
> で checkout されている。そのため merge / close commit は **base worktree 側
> で実行**し、feature worktree (`[worktree_dir]`) はその後で削除する。
> feature worktree 内で `git switch [default_branch]` を実行しても、別 worktree
> がそのブランチを保持しているため Git に拒否される。

#### Step 1: Preflight check(feature worktree で確認)

```bash
cd [worktree_dir]
test -z "$(git status --porcelain)" || { echo "ABORT: uncommitted changes in [worktree_dir]"; exit 1; }
git rev-parse --abbrev-ref HEAD | grep -qE "^[a-z]+/local-[a-z0-9]+-[0-9]+(-[a-z0-9-]+)?$" || { echo "ABORT: not on feature branch"; exit 1; }
```

未コミット変更 / feature ブランチ外なら ABORT。

#### Step 2: Base worktree を特定し、base branch を最新化

`git worktree list --porcelain` から `[default_branch]` を checkout している
worktree を抽出する。見つからなければ user が手動で base 側を準備する必要が
あるため ABORT。

```bash
# [default_branch] を checkout している worktree を取得
BASE_WT=$(git worktree list --porcelain | awk -v b="[default_branch]" '
    /^worktree / { wt=$2 }
    $0 == "branch refs/heads/" b { print wt; exit }
')
test -n "$BASE_WT" || { echo "ABORT: no worktree has [default_branch] checked out. Run 'git worktree add <path> [default_branch]' or 'git switch [default_branch]' in your main checkout first."; exit 1; }

cd "$BASE_WT"

# Step 2.1: 3 段ガードによる救済 commit 判定(Issue local-p1-16 B)
#
# 標準動線(各 skill での `kaji issue {comment,edit} --commit`)が機能していれば
# base worktree は clean。蓄積が残っている場合は LocalProvider 永続化由来の path
# のみ救済し、それ以外は ABORT する。
#
# 救済対象 (LocalProvider 命名規則):
#   - .kaji/issues/<issue_id>-<slug>/issue.md
#   - .kaji/issues/<issue_id>-<slug>/comments/<4桁seq>-<machine_id>.md
# 出典: kaji_harness/providers/local.py:586 / providers/context.py:17 / providers/local.py:36
DIRTY=$(git status --porcelain)
if [ -n "$DIRTY" ]; then
    # 条件 1: dirty path がすべて [issue_id] の永続化 whitelist に一致するか検査
    ISSUE_DIR_RE='^\.kaji/issues/[issue_id]-[a-z0-9-]+/(issue\.md|comments/[0-9]{4}-[a-z0-9]{1,16}\.md)$'
    UNRELATED=$(printf '%s\n' "$DIRTY" | awk -v re="$ISSUE_DIR_RE" '
        {
            # rename / copy ("R  old -> new") は救済対象外として ABORT に倒す
            if (match($0, /->/)) { print; next }
            # 先頭 3 文字 (status + space) を除いた残りを path として扱う
            path = substr($0, 4)
            # quoted path (path に空白等あり) は対応外として ABORT
            if (substr(path, 1, 1) == "\"") { print; next }
            if (path !~ re) { print }
        }
    ')
    if [ -n "$UNRELATED" ]; then
        echo "ABORT: dirty files outside LocalProvider persistence whitelist in base worktree $BASE_WT:"
        printf '%s\n' "$UNRELATED"
        echo "  Allowed pattern: $ISSUE_DIR_RE"
        exit 1
    fi
    # 条件 2: whitelist 命名規則の glob で限定 add + atomic commit
    #   - `comments/` ディレクトリ全体を add してはならない (note.txt 等を巻き込む)
    #   - `git commit --only` で他の staged change を HEAD に混入させない
    git add \
        ".kaji/issues/[issue_id]-"*"/issue.md" \
        ".kaji/issues/[issue_id]-"*"/comments/"[0-9][0-9][0-9][0-9]-*.md \
        2>/dev/null || true
    git commit --only \
        -m "chore(local): salvage uncommitted issue files for [issue_ref]" \
        -- \
        ".kaji/issues/[issue_id]-"*"/issue.md" \
        ".kaji/issues/[issue_id]-"*"/comments/"[0-9][0-9][0-9][0-9]-*.md \
        || { echo "ABORT: salvage commit failed"; exit 1; }
    # 条件 3: 救済後の残差を再検証 (rename/copy/rm 等の取りこぼしを検出)
    test -z "$(git status --porcelain)" || {
        echo "ABORT: residual dirty files after salvage commit in base worktree $BASE_WT:"
        git status --porcelain
        exit 1
    }
fi

# remote 設定がある場合のみ fetch + ff-only merge。
# fetch 失敗 (network 断 / 認証エラー / suspended account 等) は WARNING で skip し、
# local-only で close を完結させる (Step 6 の push も同様に warning で続行する設計と整合)。
# `kaji run` 非対話モードでは AskUserQuestion 経由のリカバリ不可のため、
# deterministic に local fallback すること。手動 push は remote 復旧後に実施。
if git remote get-url [git_remote] >/dev/null 2>&1; then
    if git fetch [git_remote] [default_branch] 2>&1; then
        git merge --ff-only "[git_remote]/[default_branch]" || { echo "ABORT: ff-only merge failed in base worktree"; exit 1; }
    else
        echo "WARNING: git fetch [git_remote] [default_branch] failed; proceeding with local-only close (manual push needed after remote recovery)"
    fi
fi
```

ABORT 条件:
- fast-forward できない (ローカル [default_branch] が [git_remote]/[default_branch] から分岐) → resolve 後に再実行
- base worktree 側に LocalProvider 永続化 whitelist 外の dirty file が残存 → 手動コミット後に再実行

WARNING 継続条件:
- `git fetch` 失敗 (remote 到達不可 / 認証失敗) → local merge は実行、push は Step 6 で warning skip

標準動線で各 skill が `kaji issue {comment,edit} --commit` を使っていれば、ここまで到達した時点で
base worktree は clean のはず。救済 commit は標準動線が機能しなかった場合の安全装置として残す。

#### Step 3: Merge 実行(base worktree 上で)

```bash
git merge --no-ff --no-edit [branch_name] || { echo "ABORT: merge conflict, resolve manually in $BASE_WT then retry"; exit 1; }
```

衝突したら ABORT。Issue は open のまま、user が手動 resolve した後で再実行する。

#### Step 4: 事後確認の移管 + Issue frontmatter 更新 + commit(base worktree 上で)

最初に「共通: ワークフロー完了後の確認項目の移管」を実行する。follow-up 作成または
親マーカー追記に失敗した場合は `ABORT` とし、親 Issue を close しない。

移管成功後、親 Issue を close する。

```bash
kaji issue close [issue_id] --reason completed
git add .kaji/issues/[issue_id]-*/issue.md
git commit -m "chore(issue): close [issue_ref]" || { echo "ABORT: commit failed"; exit 1; }
```

`--reason completed` は明示で書く(`LocalProvider.close_issue` の default も
`completed` だが、Skill markdown 上で明示することで読み手の予期外を減らす)。

**Step 4 完了で Issue close は確定**。以降の失敗は警告のみ。

#### Step 5: Cleanup(base worktree から feature worktree を削除)

base worktree に居る状態で feature worktree を削除する。`cwd == 削除対象` を
回避するため、Step 2 の `cd "$BASE_WT"` は維持したまま実行する。

```bash
git worktree remove [worktree_dir] || echo "WARNING: worktree remove failed for [worktree_dir]; manual cleanup needed"
git branch -d [branch_name] || echo "WARNING: branch delete failed for [branch_name]; manual cleanup needed"
```

#### Step 6: Push(remote 設定がある場合、base worktree から)

```bash
if git remote get-url [git_remote] >/dev/null 2>&1; then
    git push [git_remote] [default_branch] || echo "WARNING: push failed; manual push needed"
fi
```

## Verdict 出力

実行完了後、以下の形式で verdict を出力すること:

```
---VERDICT---
status: PASS
reason: |
  クローズ完了
evidence: |
  事後確認の移管要否を確認し、必要な follow-up Issue の作成または再利用と親マーカー追記を完了した。PR マージ・worktree 削除・main 最新化・Issue クローズ済み
suggestion: |
---END_VERDICT---
```

**重要**: verdict は **stdout にそのまま出力** すること。Issue コメントや Issue 本文更新とは別に、最終的な verdict ブロックは stdout に残す。

### status の選択基準

| status | 条件 |
|--------|------|
| PASS | クローズ完了 |
| ABORT | follow-up 作成・再利用・親マーカー追記の失敗(マーカー参照先が close 済みの場合を含む)、またはクローズ失敗(`kaji issue close` 失敗を含む / local merge 衝突) |

Files in this skill

  • SKILL.md22.5 KB
  • templates/follow-up-issue.md1 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…