From 792e556490495815abf1270be40150dc2578f638 Mon Sep 17 00:00:00 2001 From: che cheng Date: Fri, 14 Aug 2026 05:39:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20legacy=20config=20=E9=81=B7=E7=A7=BB?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=20+=20deprecation=20=E8=81=B2=E6=98=8E?= =?UTF-8?q?=E3=80=81milestone=20SOP=E3=80=81question=20=E8=87=AA=E5=8B=95?= =?UTF-8?q?=20@=20(#303,=20#83,=20#128)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #303 #195 只遷了寫入端,既有 config 沒有遷移路徑也沒有支援承諾 —— 三種可能狀態裡 最差的一種。實測一台使用中的機器有 32 個 repo 仍在 legacy 路徑。新增 --scan/--apply 遷移工具(兩者並存時不動任何一個),並在 config-protocol 明寫承諾:legacy 讀取不設 移除日期,但新掃描器只需要認 current。 #83 把使用者 2026-05-12 手動發現的做法寫成 SOP。Epic issue 跑 IDD lifecycle 每個 phase 都空轉(沒有單一 root cause、沒有 scope、沒有 diff、closing summary 五段填不 出實質內容)。判準:要計數用 milestone,要論述用 tracking issue。 #128 難的一半(tag 前驗證 handle)早已上線且七個 skill 強制引用,缺的是觸發點。 --type=question 現在依協定 @ 到人,並寫明三條邊界:diagnose 排除、不從姓名猜 handle、 不確定對象時不 tag(漏 tag 少一個通知,tag 錯人不可逆)。 45/45 suites 綠(新增 migrate-idd-config 13 條)。 --- .../.claude-plugin/plugin.json | 2 +- plugins/issue-driven-dev/CHANGELOG.md | 31 ++++++ plugins/issue-driven-dev/CLAUDE.md | 8 ++ .../references/config-protocol.md | 15 +++ .../references/milestone-first-tracking.md | 39 ++++++++ .../scripts/migrate-idd-config.sh | 97 +++++++++++++++++++ .../scripts/tests/migrate-idd-config/test.sh | 43 ++++++++ .../skills/idd-comment/SKILL.md | 15 +++ 8 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 plugins/issue-driven-dev/references/milestone-first-tracking.md create mode 100755 plugins/issue-driven-dev/scripts/migrate-idd-config.sh create mode 100755 plugins/issue-driven-dev/scripts/tests/migrate-idd-config/test.sh diff --git a/plugins/issue-driven-dev/.claude-plugin/plugin.json b/plugins/issue-driven-dev/.claude-plugin/plugin.json index fa9f7dc..31e51a5 100644 --- a/plugins/issue-driven-dev/.claude-plugin/plugin.json +++ b/plugins/issue-driven-dev/.claude-plugin/plugin.json @@ -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" }, diff --git a/plugins/issue-driven-dev/CHANGELOG.md b/plugins/issue-driven-dev/CHANGELOG.md index 430d58f..17cd3c4 100644 --- a/plugins/issue-driven-dev/CHANGELOG.md +++ b/plugins/issue-driven-dev/CHANGELOG.md @@ -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 diff --git a/plugins/issue-driven-dev/CLAUDE.md b/plugins/issue-driven-dev/CLAUDE.md index 61f1b56..88bf403 100644 --- a/plugins/issue-driven-dev/CLAUDE.md +++ b/plugins/issue-driven-dev/CLAUDE.md @@ -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)。 diff --git a/plugins/issue-driven-dev/references/config-protocol.md b/plugins/issue-driven-dev/references/config-protocol.md index b4af7b6..32638e1 100644 --- a/plugins/issue-driven-dev/references/config-protocol.md +++ b/plugins/issue-driven-dev/references/config-protocol.md @@ -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 | `/.claude/.idd/local.json` | **唯一的寫入目標**(#195 起)| +| legacy | `/.claude/issue-driven-dev.local.json` | **仍然讀得到,但不再被寫入**;請遷移 | + +**遷移工具**:`scripts/migrate-idd-config.sh --scan`(只報告)/`--apply`(實際搬移)。同層若兩者都存在,工具**不動**任何一個 —— 讀取端本來就是 current 勝,搬移只會有機會毀掉一份手改過的檔案。 + +**支援承諾(明文,因為缺這句話本身就是問題)**:legacy 路徑的**讀取**支援不設移除日期,但**不保證新工具會實作它**。任何新的 config 掃描器只需要認 current 路徑;若它同時認 legacy,那是選配。 + +**為什麼這句話必須寫下來**:#195 只遷了寫入端,於是「既沒有遷移、也沒有承諾永久支援」—— 三種可能狀態裡最差的一種。下游無法規劃:每個新的掃描器都得自己決定要不要寫兩份解析,而漏認一種格式的語意(在 #302 的 repo 地圖裡)是「該層不存在」,會觸發**錯誤的上收**。實測一台使用中的機器有 **32 個 repo** 仍在 legacy 路徑上。 diff --git a/plugins/issue-driven-dev/references/milestone-first-tracking.md b/plugins/issue-driven-dev/references/milestone-first-tracking.md new file mode 100644 index 0000000..298402f --- /dev/null +++ b/plugins/issue-driven-dev/references/milestone-first-tracking.md @@ -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 --milestone ""`** 把子票掛上去。新建時用 `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。 diff --git a/plugins/issue-driven-dev/scripts/migrate-idd-config.sh b/plugins/issue-driven-dev/scripts/migrate-idd-config.sh new file mode 100755 index 0000000..48064c6 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/migrate-idd-config.sh @@ -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: /.claude/issue-driven-dev.local.json +# current: /.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 diff --git a/plugins/issue-driven-dev/scripts/tests/migrate-idd-config/test.sh b/plugins/issue-driven-dev/scripts/tests/migrate-idd-config/test.sh new file mode 100755 index 0000000..3f5a4aa --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/migrate-idd-config/test.sh @@ -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 $? diff --git a/plugins/issue-driven-dev/skills/idd-comment/SKILL.md b/plugins/issue-driven-dev/skills/idd-comment/SKILL.md index 9ae897a..fb2014e 100644 --- a/plugins/issue-driven-dev/skills/idd-comment/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-comment/SKILL.md @@ -244,6 +244,21 @@ gh api repos/$OWNER/$REPO/collaborators --jq '.[] | {login, name}' \ ``` +**問題有對象時要 @ 到人(#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