From 67b842471a2e860821a319178a4c9367839b235d Mon Sep 17 00:00:00 2001 From: che cheng Date: Mon, 6 Jul 2026 22:03:52 +0800 Subject: [PATCH 1/2] feat: discussion-type issue metadata helper in idd-issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Step 3.5 (Discussion Metadata Helper) to idd-issue: after an issue is created and before attachment upload, advisory-surface GitHub right-sidebar metadata for discussion-type issues (advisor briefing / review-feedback / awaiting-decision). Lean-v1 scope (per feedback_lead_minimal — the essence, not the full 7-component v1): - Detection: --discussion flag is the sole primary trigger; body-heuristic detection is opt-in behind config discussion_metadata_heuristic (design Q1's own recommended default) so standard bug/feature issues are never subjected to the 4-prompt ceremony. - Four advisory sub-steps, all surface-only (never auto-set outward-facing metadata; unattended mode prints suggestions only, sets nothing): (a) assignee from body's tagging-collaborators-verified @login set; (b) label = config-overridable discussion_label (default `discussion`); (c) milestone only on a time-bound signal, reusing Step 4.5 machinery; (d) native GraphQL relationship DEFERRED (- [~], disclosed) — highest complexity/risk, reuses a not-yet-generalized --blocked-by code path. Fold in #142's three mandatory label safeguards: - S1 verify-by-read after `gh label create` (silent-succeed is indistinguishable from noop-on-exists — read-back is the only reliable detection). - S2 pre-add existence check for the --add-label path. - S3 UI cache-sync hint at the Step 5 report on FRESH create only — names the confusing "Invalid value ... for label" string + hard-refresh remedy. TDD: new artifact-level drift-guard fixture scripts/tests/discussion-metadata/test.sh (13 assertions, RED→GREEN); full suite 22/22 GREEN. Fixture locks the contract prose (trigger + 4 sub-steps + 3 safeguards + disclosed deferral) against silent deletion; it does not execute the AskUserQuestion helper (Claude-runtime contract, unrunnable in a shell harness). Refs #141 Refs #142 --- .../scripts/tests/discussion-metadata/test.sh | 64 ++++++++++ .../skills/idd-issue/SKILL.md | 112 ++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100755 plugins/issue-driven-dev/scripts/tests/discussion-metadata/test.sh 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..0a04bec --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/discussion-metadata/test.sh @@ -0,0 +1,64 @@ +#!/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" +# 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..df05b53 100644 --- a/plugins/issue-driven-dev/skills/idd-issue/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-issue/SKILL.md @@ -861,6 +861,105 @@ 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 + gh label list --repo "$REPO" --search "$1" --json name \ + --jq '.[] | select(.name == "'"$1"'") | .name' | grep -qx "$1" +} +# 只對非 default-set label 跑(discussion / epic / custom);bug/feature/refactor/docs 是 repo 預設,跳過。 +if ! label_exists "$LABEL_NAME"; then + echo "⚠ Label '$LABEL_NAME' 不存在於 $REPO — 選 auto-create 或 skip" + # → 併入上面 AskUserQuestion 的 option 2/3 +fi +gh issue edit "$NUMBER" --repo "$REPO" --add-label "$LABEL_NAME" # 只在 verified-exist 後 +``` + +**#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 +1166,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 才發現。 From 2158b1dfe6580d80566312a3c991e56649d4f2fc Mon Sep 17 00:00:00 2001 From: che cheng Date: Mon, 6 Jul 2026 22:30:00 +0800 Subject: [PATCH 2/2] fix: gate discussion-label add on existence check (idd-issue Step 3.5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit verify round-1(Codex 跨模型盲驗)抓到 #142 Safeguard 2 的控制流 bug: `gh issue edit --add-label` 在 label_exists 的 if 之後無條件執行, 註解寫「只在 verified-exist 後」但控制流沒 gate —— 對不存在的 label 做 --add-label 會 silently auto-create 白色 label,正是 S2 要防的 edge case。 - SKILL.md:把 --add-label gate 進 `if label_exists` 的 then 分支; else 分支導回 auto-create(S1 verify-by-read)或 skip,不落到 blind add - label_exists:改用 `.[].name | grep -qx` 精確比對,不把 name 裸插進 jq 表達式 —— 含引號的 custom label 名也安全(Codex finding 2) - drift-guard fixture:加一個 needle 鎖「不 blind add」gated 分支,把此次 修復轉成 regression guard(bash 不可 shell-run,只能鎖 prose;Codex finding 3) drift-guard: 22 suites / 0 fail(discussion-metadata 14 needles GREEN) Refs #141 Refs #142 --- .../scripts/tests/discussion-metadata/test.sh | 5 +++++ .../issue-driven-dev/skills/idd-issue/SKILL.md | 17 ++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/plugins/issue-driven-dev/scripts/tests/discussion-metadata/test.sh b/plugins/issue-driven-dev/scripts/tests/discussion-metadata/test.sh index 0a04bec..9a818ea 100755 --- a/plugins/issue-driven-dev/scripts/tests/discussion-metadata/test.sh +++ b/plugins/issue-driven-dev/scripts/tests/discussion-metadata/test.sh @@ -57,6 +57,11 @@ assert_output_grep "141 (d) native relationship deferral disclosed" "Nati 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" diff --git a/plugins/issue-driven-dev/skills/idd-issue/SKILL.md b/plugins/issue-driven-dev/skills/idd-issue/SKILL.md index df05b53..057bb8f 100644 --- a/plugins/issue-driven-dev/skills/idd-issue/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-issue/SKILL.md @@ -915,16 +915,19 @@ AskUserQuestion: **#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 - gh label list --repo "$REPO" --search "$1" --json name \ - --jq '.[] | select(.name == "'"$1"'") | .name' | grep -qx "$1" +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 - echo "⚠ Label '$LABEL_NAME' 不存在於 $REPO — 選 auto-create 或 skip" - # → 併入上面 AskUserQuestion 的 option 2/3 +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 -gh issue edit "$NUMBER" --repo "$REPO" --add-label "$LABEL_NAME" # 只在 verified-exist 後 ``` **#142 Safeguard 1 — VERIFY by reading back**(auto-create 路徑)。`gh label create` 成功時**無 stdout**,且對「已存在」case 也 silently noop —— exit code + stdout 無法區分「剛建成」「早就有」「靜默失敗」。唯一可靠偵測是建完再讀回: