Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/issue-driven-dev/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "issue-driven-dev",
"description": "v2.102.2: Deep Research light integration (#277, ruling b). idd-diagnose gains a non-binding pointer (the #111 superpowers hand-off shape: pure suggestion, no presence check, no dependency) fired when the diagnosis's quality depends on facts OUTSIDE the repo — with trigger examples AND counter-examples (the overly-broad-signal risk). Output flows back via '/idd-comment --type note' as summary + link, never full text (#116) — that is what keeps external research inside the audit trail. Both real-user misconceptions get canonical answers where they lived: research attaches AT diagnose (not after plan), and research vs implement are different phases' work, not substitutes. usecase-routing scenario 32 + a three-row internal-corpus vs external-world boundary table (idd-find / idd-ask / Deep Research). Deep integration stays a recorded residue until a plugin-dependable primitive exists.",
"version": "2.104.0",
"version": "2.105.0",
"author": {
"name": "Che Cheng"
},
Expand Down
31 changes: 31 additions & 0 deletions plugins/issue-driven-dev/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,37 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.105.0] - 2026-08-14

### Added

- **`scripts/migrate-idd-config.sh` + an explicit deprecation statement (#303)** — #195 moved config *write-back* to
`.claude/.idd/local.json` and moved nothing that already existed, leaving both layouts coexisting with **no migration
and no support promise** — the worst of the three possible states, because a downstream tool cannot tell whether the
legacy path is permanent or about to vanish. Measured on one real machine: **32 repos** still on the legacy path.
Every new config scanner therefore has to parse both, and each gets its own chance to miss one — in #302's repo-map
scanner, missing a config means "this layer does not exist" and triggers the wrong upward resolution. The script has
`--scan` (default, writes nothing) and `--apply`; when both paths exist it **touches neither**, because the reader
already prefers the current one and moving could destroy a hand-edited file. `config-protocol.md` now states the
promise plainly: legacy reads keep working with no removal date, but **new scanners only need the current path**.

- **`references/milestone-first-tracking.md` (#83)** — writes down the SOP the user found by hand in a 2026-05-12
dogfood session (「還是你可以創 milestone 來追蹤」). Epic issues are an anti-pattern for cross-cluster tracking because
every IDD phase degenerates on them: no single root cause to diagnose, no scope for the guard to police, no diff to
verify, and a closing summary whose five sections cannot be filled with anything real. Milestones carry the one thing
actually needed — "these finish together" — and GitHub counts progress itself. The dividing line: **counting →
milestone, argument → `tracking` issue**; neither runs implement or verify.

### Fixed

- **`--type=question` now tags the person it is asking (#128)** — the user's point was simple: 「如果要問一個人問題,要在
問題上自動tag他吧(diagonose除外)」. The hard half — verifying a handle before mentioning it — already shipped as
`rules/tagging-collaborators.md`, mandatory across seven skills; what was missing was the trigger. `idd-comment
--type=question` now follows that protocol when the question is directed at someone, with three boundaries stated:
`/idd-diagnose` is excluded (it is analysis for oneself, not a question), handles are never guessed from a name (a
mention cannot be withdrawn), and an unclear addressee means **no tag** — a missing mention costs one notification,
the wrong mention is an irreversible interruption.

## [2.104.0] - 2026-08-14

### Fixed
Expand Down
8 changes: 8 additions & 0 deletions plugins/issue-driven-dev/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -522,3 +522,11 @@ IDD 不是把 TDD + SDD + issue tracking 拼在一起的 combo —

- Update after changes: `/plugin-tools:plugin-update issue-driven-dev`
- Health check: `/plugin-tools:plugin-health`

## 跨 cluster 追蹤用 milestone,不用 Epic issue(#83)

一組要一起完成的 issue,用 GitHub **milestone** 追蹤;**不要**開 Epic issue 當母票。Epic 跑 IDD lifecycle 不合適 —— 它沒有單一 root cause、沒有可實作的 scope、沒有可驗證的 diff,每個 phase 套上去都是空轉。

判準一句話:**要計數就用 milestone,要論述就用 `tracking` phase 的 tracker issue。** 兩者都不跑 implement / verify。

完整 SOP 見 [`references/milestone-first-tracking.md`](references/milestone-first-tracking.md)。
15 changes: 15 additions & 0 deletions plugins/issue-driven-dev/references/config-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -729,3 +729,18 @@ User runs `/idd-issue`, attaches label `cross-package`. Re-resolve picks the gro
2. Creates tracking issue in `bar` → `#15`, body starts `Tracking primary: PsychQuant/foo#42`
3. Creates tracking issue in `glue` → `#8`, body starts `Tracking primary: PsychQuant/foo#42`
4. Comments on `foo#42`: `Tracked in: PsychQuant/bar#15, PsychQuant/glue#8`

## Legacy config path — deprecation (#303)

兩個路徑並存:

| 形式 | 路徑 | 狀態 |
|---|---|---|
| current | `<repo>/.claude/.idd/local.json` | **唯一的寫入目標**(#195 起)|
| legacy | `<repo>/.claude/issue-driven-dev.local.json` | **仍然讀得到,但不再被寫入**;請遷移 |

**遷移工具**:`scripts/migrate-idd-config.sh --scan`(只報告)/`--apply`(實際搬移)。同層若兩者都存在,工具**不動**任何一個 —— 讀取端本來就是 current 勝,搬移只會有機會毀掉一份手改過的檔案。

**支援承諾(明文,因為缺這句話本身就是問題)**:legacy 路徑的**讀取**支援不設移除日期,但**不保證新工具會實作它**。任何新的 config 掃描器只需要認 current 路徑;若它同時認 legacy,那是選配。

**為什麼這句話必須寫下來**:#195 只遷了寫入端,於是「既沒有遷移、也沒有承諾永久支援」—— 三種可能狀態裡最差的一種。下游無法規劃:每個新的掃描器都得自己決定要不要寫兩份解析,而漏認一種格式的語意(在 #302 的 repo 地圖裡)是「該層不存在」,會觸發**錯誤的上收**。實測一台使用中的機器有 **32 個 repo** 仍在 legacy 路徑上。
39 changes: 39 additions & 0 deletions plugins/issue-driven-dev/references/milestone-first-tracking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Milestone-first cross-cluster tracking (#83)

**用 GitHub 原生的 milestone 追蹤跨 cluster 的工作,不要用 Epic issue。**

## 由來

2026-05-12 的 dogfood session(`PsychQuantHsu/psychophysical_representations`),使用者一句話切開了一個卡住的 pattern:

> 「還是你可以創 milestone 來追蹤」

當時的處境是:有一組互相關聯的 issue 要一起追,直覺做法是開一張 Epic issue 當母票。但 Epic 跑 IDD lifecycle **不合適** —— 它沒有可診斷的 root cause、沒有可實作的 scope、沒有可驗證的完成條件,於是每一個 IDD phase 套在它身上都是空轉。

## 為什麼 Epic issue 是 anti-pattern

IDD 的每個 artifact 都預設它的 issue 有一個**單一可交付物**:

| Phase | 對 Epic 的意義 |
|---|---|
| `diagnose` | 沒有單一 root cause 可查 —— 診斷會退化成「列出子票」 |
| `implement` | 沒有東西可實作,scope guard 無從判斷越界 |
| `verify` | 沒有 diff 可驗;6-AI ensemble 對它是 dead weight |
| `close` | closing summary 的五段式(Problem / Root Cause / Solution / Verification / Changes)全部填不出實質內容 |

結果是 Epic 卡在 `diagnosed` 或被迫寫一份空洞的 lifecycle 紀錄,而它真正的功能(「這幾張要一起完成」)根本不需要 lifecycle。

## SOP

1. **建 milestone,不建 Epic issue。** 標題就是那組工作的名字,description 寫「完成條件」(不是子票清單 —— 清單由 GitHub 自動維護)。
2. **子票照常各自跑完整 IDD lifecycle。** 它們是真的 issue,有 root cause、有 diff、有 verify。
3. **`gh issue edit <N> --milestone "<name>"`** 把子票掛上去。新建時用 `gh issue create --milestone`。
4. **進度用 GitHub 自己的計數**(`gh api repos/{o}/{r}/milestones` 的 `open_issues` / `closed_issues`),不要人工維護一份會過期的 checklist。
5. **milestone 關閉 = 那組工作完成**,不需要也不應該有一份「Epic 的 closing summary」—— 每張子票已經各自留了自己的。

## 什麼時候仍該用 issue 而非 milestone

- **cluster PR**:多張 issue 共用一個 PR 時走 `idd-implement #A #B #C --pr`,那是 PR 層的分組,與 milestone 正交(兩者可以並用)。
- **north-star / tracker**:需要敘述性脈絡(為什麼這條線存在、目前的假設是什麼)而不只是計數時,`tracking` phase 的 issue 仍是對的工具(v2.82.0+ #179)。milestone 只有標題與描述,承載不了論述。

判準:**要計數就用 milestone,要論述就用 tracker issue。** 兩者都不該跑 implement / verify。
97 changes: 97 additions & 0 deletions plugins/issue-driven-dev/scripts/migrate-idd-config.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
# migrate-idd-config.sh — move legacy IDD config to the current path (#303).
#
# #195 moved the WRITE-BACK side to `.claude/.idd/local.json`. It did not move
# anything that already existed, so both layouts have coexisted since, with no
# migration and no deprecation statement — the worst of the three possible
# states, because a consumer cannot tell whether the legacy form is supported
# forever or about to disappear.
#
# legacy: <repo>/.claude/issue-driven-dev.local.json
# current: <repo>/.claude/.idd/local.json
#
# Measured on one real machine (two work roots, maxdepth 8, excluding
# node_modules/.git/.build): 32 repos still on the legacy path. Every new tool
# that scans config has to parse both, and each one gets its own chance to miss
# a format — #302's repo-map scanner already listed that as a HIGH risk, where
# missing a config means "this layer does not exist" and triggers the wrong
# upward resolution.
#
# Usage:
# migrate-idd-config.sh --scan [root ...] # report only, no writes (default)
# migrate-idd-config.sh --apply [root ...] # move + leave a pointer behind
#
# Defaults to scanning ~/Developer if no root is given.
# ALWAYS exits 0 on scan; --apply exits non-zero only if a move failed.

set -u

MODE="scan"
ROOTS=()
while [ $# -gt 0 ]; do
case "$1" in
--scan) MODE="scan"; shift ;;
--apply) MODE="apply"; shift ;;
-h|--help) sed -n '2,/^$/p' "$0" | sed 's/^#\{1,\} \{0,1\}//'; exit 0 ;;
*) ROOTS+=("$1"); shift ;;
esac
done
[ ${#ROOTS[@]} -eq 0 ] && ROOTS=("$HOME/Developer")

LEGACY_NAME="issue-driven-dev.local.json"
found=0
migrated=0
skipped=0
failed=0

for root in "${ROOTS[@]}"; do
[ -d "$root" ] || { echo "note: not a directory, skipping: $root" >&2; continue; }
# -prune the heavy directories rather than filtering after the fact.
while IFS= read -r legacy; do
found=$((found + 1))
dir=$(dirname "$legacy") # .../.claude
current="$dir/.idd/local.json"
repo=$(dirname "$dir")

if [ -f "$current" ]; then
# Both exist. The reader already prefers the current path, so moving would
# change nothing and could destroy a hand-edited legacy file. Report, do
# not touch.
echo " = both present, current wins (left alone): $repo"
skipped=$((skipped + 1))
continue
fi

if [ "$MODE" = "scan" ]; then
echo " → would migrate: $repo"
continue
fi

if ! mkdir -p "$dir/.idd" 2>/dev/null; then
echo " ✗ cannot create $dir/.idd" >&2; failed=$((failed + 1)); continue
fi
if ! mv "$legacy" "$current" 2>/dev/null; then
echo " ✗ move failed: $legacy" >&2; failed=$((failed + 1)); continue
fi
# Leave a breadcrumb: a repo whose config silently relocated is confusing to
# anyone who bookmarked the old path or greps for it.
printf '%s\n' \
"This file moved to .claude/.idd/local.json (#303, $(date +%Y-%m-%d))." \
"The old path is no longer written by any IDD skill." \
> "$legacy.moved"
echo " ✓ migrated: $repo"
migrated=$((migrated + 1))
done < <(find "$root" \
\( -name node_modules -o -name .git -o -name .build -o -name .venv \) -prune -o \
-type f -name "$LEGACY_NAME" -print 2>/dev/null)
done

echo ""
echo "legacy configs found: $found"
if [ "$MODE" = "scan" ]; then
echo "(scan only — re-run with --apply to migrate)"
exit 0
fi
echo "migrated: $migrated left alone (both present): $skipped failed: $failed"
[ "$failed" -eq 0 ] || exit 1
exit 0
43 changes: 43 additions & 0 deletions plugins/issue-driven-dev/scripts/tests/migrate-idd-config/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Test: migrate-idd-config.sh moves legacy config without destroying anything (#303).
set -u
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPT="$(cd "$HERE/../.." && pwd)/migrate-idd-config.sh"
. "$(cd "$HERE/../../lib" && pwd)/assert-helpers.sh"

S=$(mktemp -d)
trap 'rm -rf "$S"' EXIT
mkdir -p "$S/a/.claude" "$S/b/.claude/.idd" "$S/n/node_modules/x/.claude"
echo '{"github_repo":"o/a"}' > "$S/a/.claude/issue-driven-dev.local.json"
echo '{"github_repo":"o/b-legacy"}' > "$S/b/.claude/issue-driven-dev.local.json"
echo '{"github_repo":"o/b-cur"}' > "$S/b/.claude/.idd/local.json"
echo '{"github_repo":"o/noise"}' > "$S/n/node_modules/x/.claude/issue-driven-dev.local.json"

SCAN=$(bash "$SCRIPT" --scan "$S" 2>&1); SRC=$?
assert_exit "scan exits 0" 0 "$SRC"
assert_grep "scan reports the migratable repo" "would migrate" "$SCAN"
refute_grep "scan does not write anything" "✓ migrated" "$SCAN"
refute_grep "node_modules is pruned, not reported" "noise" "$SCAN"
require "scan left the legacy file in place" \
test -f "$S/a/.claude/issue-driven-dev.local.json"

OUT=$(bash "$SCRIPT" --apply "$S" 2>&1); ARC=$?
assert_exit "apply exits 0 when nothing failed" 0 "$ARC"
require "legacy file moved to the current path" test -f "$S/a/.claude/.idd/local.json"
refute "legacy file no longer at the old path" test -f "$S/a/.claude/issue-driven-dev.local.json"
require "a breadcrumb is left behind" test -f "$S/a/.claude/issue-driven-dev.local.json.moved"

# The destructive case this script must never do: clobber an existing current
# config with a stale legacy one. Both present -> leave both alone.
assert_grep "both-present is reported, not migrated" "both present" "$OUT"
require "existing current config is untouched" \
bash -c 'grep -q "b-cur" "$0/b/.claude/.idd/local.json"' "$S"
require "its legacy sibling is also left in place" \
test -f "$S/b/.claude/issue-driven-dev.local.json"

# node_modules must stay pruned even on apply
require "node_modules config was not touched" \
test -f "$S/n/node_modules/x/.claude/issue-driven-dev.local.json"

print_summary "migrate-idd-config"
exit $?
15 changes: 15 additions & 0 deletions plugins/issue-driven-dev/skills/idd-comment/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,21 @@ gh api repos/$OWNER/$REPO/collaborators --jq '.[] | {login, name}' \
<!-- idd:comment type=question date=YYYY-MM-DD status=open -->
```

**問題有對象時要 @ 到人(#128)**:`--type=question` 的本質是**向某個人提問**。使用者原話:

> 「如果要問一個人問題,要在問題上自動tag他吧(diagonose除外,所以你要知道使用電腦的人是哪一個使用者)」

所以:問題若指向特定的人(body 出現某人的名字、或使用者在呼叫時說「問 X」),**必須**依 [`rules/tagging-collaborators.md`](../../rules/tagging-collaborators.md) 的五步協定 @ 到他的 GitHub login,並把 mention 放在問題本文裡(不是附在最後)。

**三條邊界**:

1. **`/idd-diagnose` 排除** —— 使用者明確排除了它。diagnose 是給自己看的分析,不是提問。
2. **不得從姓名猜 handle。** 這正是 tagging 協定存在的理由:`@JaneDoe` 猜錯會通知到無關的人,而 mention **無法撤回**。查不到就照協定走 AskUserQuestion,不要賭。
3. **不確定問題是對誰時,不 tag。** 沒有 mention 的 open question 只是少一個通知;tag 錯人是不可逆的打擾。

```
```

#### Template: `correction`

```markdown
Expand Down
Loading