Skip to content
Open
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
69 changes: 69 additions & 0 deletions plugins/issue-driven-dev/scripts/tests/discussion-metadata/test.sh
Original file line number Diff line number Diff line change
@@ -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)"
115 changes: 115 additions & 0 deletions plugins/issue-driven-dev/skills/idd-issue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 @<login>` + 「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:@<login>。設為 assignee?"
options:
- "Yes, assign @<login>" → gh issue edit "$NUMBER" --repo "$REPO" --add-assignee "<login>"
- "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 的「資料保留鐵律」:來源中**任何附件都要全部上傳**,不論張數、不論格式。
Expand Down Expand Up @@ -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 才發現。
Expand Down
Loading