diff --git a/plugins/issue-driven-dev/scripts/tests/discussion-metadata/test.sh b/plugins/issue-driven-dev/scripts/tests/discussion-metadata/test.sh new file mode 100755 index 0000000..9a818ea --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/discussion-metadata/test.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# test.sh — drift-guard for the discussion-type metadata helper in idd-issue +# (PsychQuant/issue-driven-development#141 feature + #142 bug-prevention safeguards). +# +# SCOPE (artifact-level, like session-start-commit-rule/test.sh): this fixture +# asserts the SHIPPED SKILL.md PROSE that describes Step 3.5. It does NOT execute +# the AskUserQuestion helper (that is a Claude-runtime tool contract, unrunnable +# in a shell harness). What it locks is that the helper's contract — the +# `--discussion` trigger, the four advisory sub-steps, #142's 3 non-negotiable +# label safeguards, and the disclosed native-relationship deferral — cannot be +# silently deleted or reworded out of existence without turning this suite RED. +# +# LEAN-V1 (per feedback_lead_minimal): detection collapses to the `--discussion` +# flag + a config opt-in for the body heuristic (design Q1's own recommended +# default); the native-relationship GraphQL picker is DEFERRED (highest risk, +# reuses a not-yet-generalized code path) and disclosed as such. The fixture +# asserts the deferral is DOCUMENTED, not that the picker exists — a deferred +# item that vanishes from the prose is drift too. +# +# Usage: bash test.sh (exit 0 = all pass, 1 = any fail) +set -u + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SKILL="$HERE/../../../skills/idd-issue/SKILL.md" + +HELPERS="$HERE/../../lib/assert-helpers.sh" +[ -f "$HELPERS" ] || { echo "✗ missing $HELPERS — cannot run suite" >&2; exit 1; } +. "$HELPERS" + +echo "discussion-metadata helper drift-guard (#141 + #142)" + +assert_file_exists "idd-issue SKILL.md exists" "$SKILL" + +# ── #141: detection contract ──────────────────────────────────────────────── +# --discussion is the PRIMARY (lean-v1 only) trigger; the body heuristic is +# opt-in behind a config key so a standard bug/feature issue never gets the +# 4-prompt ceremony (design Q1). +assert_output_grep "141 detection: --discussion flag documented" "--discussion" "$SKILL" +assert_output_grep "141 detection: body heuristic is config opt-in" "discussion_metadata_heuristic" "$SKILL" + +# ── #141: the helper section itself ───────────────────────────────────────── +assert_output_grep "141 helper: Step 3.5 section header present" "Step 3.5: Discussion Metadata Helper" "$SKILL" +assert_output_grep "141 helper: bootstrap stage task named" "discussion_metadata_helper" "$SKILL" + +# ── #141: advisory sub-steps (surface-only, never auto-set) ────────────────── +# (a) assignee reused from the tagging-collaborators-verified @login set. +assert_output_grep "141 (a) assignee: --add-assignee path documented" "--add-assignee" "$SKILL" +# (b) label default is config-overridable (design Q2/Q3 + #142 Q3). +assert_output_grep "141 (b) label: config-overridable discussion_label" "discussion_label" "$SKILL" +# (c) milestone only on a time-bound signal — reuses Step 4.5 machinery. +assert_output_grep "141 (c) milestone: time-bound gate documented" "time-bound" "$SKILL" +# (d) native relationship DEFERRED (lean cut) — deferral must be DISCLOSED. +assert_output_grep "141 (d) native relationship deferral disclosed" "Native relationship suggestion (deferred" "$SKILL" + +# ── #142: three non-negotiable label safeguards baked into Step 3.5 ────────── +# S1: never trust `gh label create`'s silent success — verify by reading back. +assert_output_grep "142 S1: verify-by-read after label create" "VERIFY by reading back" "$SKILL" +# S2: pre-add existence check for the --add-label path. +assert_output_grep "142 S2: pre-add label existence check" "Pre-add existence check" "$SKILL" +# S2 control-flow (verify round-1 regression guard, #248): the --add-label MUST be +# GATED on label_exists — an unconditional blind add silently auto-creates a white +# label, the exact edge #142 S2 exists to prevent. Prose-drift can't be caught for +# free (bash isn't shell-run here), so lock the distinctive gated-branch phrase. +assert_output_grep "142 S2: --add-label gated on existence (no blind add)" "不 blind add" "$SKILL" +# S3: UI cache-sync hint — the exact confusing string + the remedy. +assert_output_grep "142 S3: cache-lag hint names the 'Invalid value' string" "Invalid value" "$SKILL" +assert_output_grep "142 S3: cache-lag hint gives hard-refresh remedy" "hard refresh" "$SKILL" + +print_summary "discussion-metadata (#141 + #142)" diff --git a/plugins/issue-driven-dev/skills/idd-issue/SKILL.md b/plugins/issue-driven-dev/skills/idd-issue/SKILL.md index 959e0da..057bb8f 100644 --- a/plugins/issue-driven-dev/skills/idd-issue/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-issue/SKILL.md @@ -861,6 +861,108 @@ EOF ✓ Cross-link comment added to PsychQuant/foo#42 ``` +### Step 3.5: Discussion Metadata Helper(討論型 issue,v2.93.0+,#141 + #142) + +**目的**:討論型 issue(advisor briefing / review-feedback / 等決定 / 等 collaborator 回應)建完後,現行 skill 不 surface GitHub right-sidebar metadata(assignee / label / milestone)。本 step 在 issue 已建立(有 `$NUMBER`)之後、Step 4 附件上傳之前,**advisory** 地把這些 metadata surface 出來。 + +> **Lean-v1 scope(#141 完整 spec 的精簡本質)**:detection 只收 `--discussion` flag(+ 一個 config opt-in 的 body heuristic);metadata 只做 assignee / label / milestone 三項 advisory;**Native relationship suggestion (deferred — 見下方 (d))**。這是刻意的最小切面,不是遺漏。 + +#### Detection:何時啟動本 helper + +Lean-v1 **只有兩個** trigger(design Q1 自身建議的預設): + +1. **Primary — explicit flag**:invocation 帶 `--discussion`(或 `--type discussion`)。 +2. **Opt-in fallback — body heuristic**:**預設 off**,只有 config `.claude/issue-driven-dev.local.json` 設 `"discussion_metadata_heuristic": true` 時,才對 body 掃 plain-language signal(`cc @` + 「briefing / summary / 等決定 / review feedback」+ ≥3 個 `#N` reference)。關掉時,標準 bug/feature issue **絕不**被這 4-prompt ceremony 打擾。 + +命中任一 → 在 bootstrap list 補一個 **conditional** stage task(只有命中才建;標準 case 不建): + +``` +TaskCreate(name="discussion_metadata_helper", + description="Step 3.5 (#141): detection 命中 → advisory assignee / label / milestone。surface-only,unattended 絕不 auto-set outward-facing metadata。") +``` + +#### Interaction 模式(surface-only 契約,CRITICAL) + +- **attended**:以下每個 sub-step 用 `AskUserQuestion`(Claude runtime tool,非 shell)問使用者,選 Yes 才實際 `gh issue edit`。 +- **unattended**(`/idd-all --pr` 等):`AskUserQuestion` 被抑制 → **不呼叫任何 `gh issue edit --add-assignee/--add-label/--milestone`**,改成在 Step 5 report 印一段「建議的 metadata(未套用)」讓人事後手動決定。**outward-facing metadata 在 unattended 下絕不 auto-set**(#141 Key Decision)。 + +#### (a) Assignee suggestion + +候選 **只來自 body 內已通過 [`rules/tagging-collaborators.md`](../../rules/tagging-collaborators.md) 5-step protocol 的 `@login`**(Step 2.6 已驗證的 set)—— 不重新猜、不從 memory 撈。 + +``` +AskUserQuestion: + "偵測到討論型 issue,body 內驗證過的 recipient:@。設為 assignee?" + options: + - "Yes, assign @" → gh issue edit "$NUMBER" --repo "$REPO" --add-assignee "" + - "Pick different assignee" → 二層:列 collaborators(gh api repos/$REPO/collaborators) + - "Skip — 不設 assignee" +``` + +#### (b) Label suggestion(#142 Safeguard 1 + 2 baked in) + +label 名稱 **config-overridable**:讀 `discussion_label`(`.claude/issue-driven-dev.local.json`,物件形 `{name,color,description}` 或純字串;預設 `discussion` / `0E8A16` / "Open question awaiting decision or input")。 + +``` +AskUserQuestion: + "加上 `$LABEL_NAME` label?(區分於 implementation issue)" + options: + - "Yes, add(label 已存在)" + - "Yes + auto-create if missing" + - "Skip" +``` + +**#142 Safeguard 2 — Pre-add existence check**(`--add-label` 路徑,label 已存在的假設要先驗)。`gh issue edit --add-label X` 對不存在的 label 行為 inconsistent(有時 error、有時 server-side 用 default 白色 silently auto-create),故 add 前先讀: + +```bash +label_exists() { # $1=name — exact-match probe(--search 是 fuzzy,靠 grep -qx 收斂成精確比對, + # 且不把 name 裸插進 jq 表達式 → 對含引號的 custom label 名也安全) + gh label list --repo "$REPO" --search "$1" --json name --jq '.[].name' | grep -qx "$1" +} +# 只對非 default-set label 跑(discussion / epic / custom);bug/feature/refactor/docs 是 repo 預設,跳過。 +if label_exists "$LABEL_NAME"; then + gh issue edit "$NUMBER" --repo "$REPO" --add-label "$LABEL_NAME" # verified-exist → 安全 add +else + echo "⚠ Label '$LABEL_NAME' 不存在於 $REPO — 不 blind add(--add-label 對不存在 label 行為 inconsistent," + echo " 有時 server-side 用 default 白色 silently auto-create,正是 #142 S2 要防的 edge case)。" + echo " → 回上面 AskUserQuestion:option 2(auto-create,走下方 Safeguard 1 的 verify-by-read)或 option 3(skip)。" + # 兩條路都不落到未驗證的 blind add:auto-create 建好 + verify 後才 add;skip 則不 add。 +fi +``` + +**#142 Safeguard 1 — VERIFY by reading back**(auto-create 路徑)。`gh label create` 成功時**無 stdout**,且對「已存在」case 也 silently noop —— exit code + stdout 無法區分「剛建成」「早就有」「靜默失敗」。唯一可靠偵測是建完再讀回: + +```bash +gh label create "$LABEL_NAME" --repo "$REPO" \ + --description "$LABEL_DESC" --color "$LABEL_COLOR" 2>/dev/null +if label_exists "$LABEL_NAME"; then + echo "✓ Label '$LABEL_NAME' verified in $REPO" + FRESH_LABEL_CREATED=1 # 給 Step 5 Safeguard 3 hint 用(只在真的新建時提示) +else + echo "✗ ABORT: gh label create 回報成功但 label 不在 list — 查 API quota / 權限 / 網路後重跑" + return 1 +fi +``` + +> GitHub label API 是 strongly consistent(建完立即可讀),**first read 即可**,不需 retry backoff(#142 Q1)。cache lag 只發生在 web UI autocomplete,見 Step 5 Safeguard 3。 + +#### (c) Milestone suggestion(只在偵測到 time-bound signal) + +僅當 body 含 **time-bound** deadline reference(「next meeting」「下次 advisor meeting」「下週」「month-end」等)才問;否則整段 skip(討論 issue 多數不綁時間)。命中時 reuse 既有 **Step 4.5 auto-milestone machinery**(`gh api repos/$REPO/milestones` 建 / 列、`--milestone` 指派),不重造: + +``` +AskUserQuestion: + "偵測到 time-bound 討論(提及 'next advisor meeting')。掛 milestone?" + options: + - "Pick existing milestone" → 列 open milestones + - "Create new milestone" → 二層問 title(+ 選填 due date,空=open-ended,#141 Q3) + - "Skip" +``` + +#### (d) Native relationship suggestion (deferred — lean-v1 不實作,disclosed) + +- [~] **Native relationship suggestion (deferred)** — body 內 `#N` 用 GitHub 2024+ GraphQL(`addBlockedByDependency` 等)建 structured relationship,比 body text reference 多 sidebar backlink。**本 cluster 刻意不做**,理由:(1) 複雜度/風險最高(需 relationship-type picker UI);(2) #141 spec 自身將其框為「reuse v2.52.0+ `--blocked-by` code path」的後續工作,而該 path 尚未一般化成可獨立呼叫的 primitive;(3) design Q4 裁定 body text reference 本來就保留(與 native backlink 互補不重複),所以延後不損失現有 inline context。點亮條件:`--blocked-by` 的 GraphQL mutation 抽成可重用 helper 後,再接本 (d)。 + ### Step 4: 附加所有原始素材(鐵律:預設全保留) > 引用 Step 1 的「資料保留鐵律」:來源中**任何附件都要全部上傳**,不論張數、不論格式。 @@ -1067,6 +1169,19 @@ options: 輸出:issue number、URL、labels、type。 如果有 milestone:輸出 milestone name、URL、issue count。 +**#142 Safeguard 3 — label cache-sync hint(只在 Step 3.5 真的新建 label 時,即 `FRESH_LABEL_CREATED=1`;既有 label reuse 不印)**: + +``` +✓ Created label '$LABEL_NAME' (#$LABEL_COLOR) + assigned to issue #$NUMBER + +ℹ GitHub Web UI 的 label autocomplete dropdown 可能要 hard refresh (Cmd+Shift+R) + 才會在 issue filter 看到此 label。若 UI 顯示 "Invalid value $LABEL_NAME for label" + —— 不是 bug,是 client-side autocomplete cache lag。filter 實際仍 work, + gh CLI 仍可用 `--label $LABEL_NAME`。 +``` + +> **為什麼 preventive**:新建 label 後第一個進 issue list 的人常看到「Invalid value … for label」就誤判 skill 出 bug(實測 Liu-thesis discussion label setup 踩過)。issue body 的 label field / API / CLI 全 work,純 UI cache lag。先講清楚省一次 debugging。 + **回顯 render 的詮釋(v2.64.0+, #103)**:除了上面的 metadata,**必須**把 AI 自己*產出*的詮釋回顯給使用者 —— issue body 的 `## Type` / `## Expected` / `## Actual` 三段 + plain-language interpretation。使用者已給過的逐字「Original text」不重複貼。 > **為什麼**:建 issue 是一個 NSQL `run → report` 操作(見 repo `CLAUDE.md`「Reference Projects: NSQL」)。建 issue 可逆,不需要 confirm gate;但 report 必須陳述「我做了什麼」,不只「它在哪」。回顯 AI render 的詮釋,讓 misparse(AI 把意圖讀錯)一眼可抓,不用點進 URL 才發現。