From d10ad52c6e015fc23376af19818718925c7563ab Mon Sep 17 00:00:00 2001 From: che cheng Date: Sat, 11 Jul 2026 10:44:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Discussions=20intake=20bridge=20?= =?UTF-8?q?=E2=80=94=20idd-list=20--discussions=20+=20idd-issue=20--from-d?= =?UTF-8?q?iscussion=20(#221)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub Discussions (Q&A/Ideas) are a real intake channel IDD was blind to (che-ical-mcp discussion 105: a whole bug lifecycle invisible to idd-list). Adds opt-in surfacing (probe -> filter category+unanswered -> dedup vs issue refs -> dedicated actionable block; graceful no-op when disabled) and Discussion-seeded filing (## Provenance + verbatim blockquote; back-reference reply draft-and-confirm, unattended draft-only). Cardinal rule: never auto-file, never auto-post. Shared contract + GraphQL templates live once in references/discussions-intake.md. Spectra change discussions-intake-bridge (6/6 tasks, TDD RED->GREEN); drift-guard 30 assertions; full suite green. Refs #221 --- .../discussions-intake-bridge/.openspec.yaml | 4 + .../discussions-intake-bridge/design.md | 41 +++++++ .../discussions-intake-bridge/proposal.md | 35 ++++++ .../specs/discussions-intake/spec.md | 53 +++++++++ .../discussions-intake-bridge/tasks.md | 17 +++ plugins/issue-driven-dev/README.md | 4 + .../references/discussions-intake.md | 108 ++++++++++++++++++ .../references/usecase-routing.md | 1 + .../scripts/tests/discussions-intake/test.sh | 66 +++++++++++ .../skills/idd-issue/SKILL.md | 15 ++- .../issue-driven-dev/skills/idd-list/SKILL.md | 16 +++ 11 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 openspec/changes/discussions-intake-bridge/.openspec.yaml create mode 100644 openspec/changes/discussions-intake-bridge/design.md create mode 100644 openspec/changes/discussions-intake-bridge/proposal.md create mode 100644 openspec/changes/discussions-intake-bridge/specs/discussions-intake/spec.md create mode 100644 openspec/changes/discussions-intake-bridge/tasks.md create mode 100644 plugins/issue-driven-dev/references/discussions-intake.md create mode 100755 plugins/issue-driven-dev/scripts/tests/discussions-intake/test.sh diff --git a/openspec/changes/discussions-intake-bridge/.openspec.yaml b/openspec/changes/discussions-intake-bridge/.openspec.yaml new file mode 100644 index 0000000..f89d908 --- /dev/null +++ b/openspec/changes/discussions-intake-bridge/.openspec.yaml @@ -0,0 +1,4 @@ +schema: spec-driven +created: 2026-07-11 +created_by: che cheng +created_with: claude diff --git a/openspec/changes/discussions-intake-bridge/design.md b/openspec/changes/discussions-intake-bridge/design.md new file mode 100644 index 0000000..595c8e2 --- /dev/null +++ b/openspec/changes/discussions-intake-bridge/design.md @@ -0,0 +1,41 @@ +## Context + +Two prose SKILLs (idd-list, idd-issue) gain Discussions awareness through GraphQL (Discussions have no REST API). The direction was fully settled by the issue author's own design constraints in #221's body plus the 2026-07-06 diagnosis; this design records the operational decisions the implementer needs. + +## Goals / Non-Goals + +**Goals**: opt-in surfacing in idd-list; Discussion-seeded filing in idd-issue; one shared normative contract + GraphQL templates in a reference doc; drift-guard test. + +**Non-Goals**: sentiment-based resolution detection; auto-filing; always-on fetch; any Discussions write beyond the single back-reference reply. (See proposal Non-Goals.) + +## Decisions + +### D1 — One shared reference doc owns the contract and the GraphQL + +Both skills cite `references/discussions-intake.md` instead of each embedding its own query text. The doc carries: (a) the three normative constraints — **no-auto-file** (surface + human judge), **dedup** (a Discussion referenced by any existing issue, open or closed, is not re-flagged), **resolution-detection** (`answerChosenAt != null` → not actionable; sentiment reading is out of scope); (b) the `hasDiscussionsEnabled` probe query; (c) the discussions list query (first 50, fields: number, title, url, category name, answerChosenAt, updatedAt, author login); (d) the single-discussion fetch query (title, body, url, author, category, answerChosenAt) for idd-issue seeding. Rationale: two copies of a GraphQL string is the drift the plugin's own deep-integration rule forbids. + +### D2 — idd-list surfacing is a new Step 2.7 (opt-in, after PR fetch, before phase extraction) + +`--discussions` flag only (no config key in v1). Flow: probe `hasDiscussionsEnabled` → false prints one line `(discussions disabled on this repo — skip)` and continues; true → fetch → filter category ∈ {Q&A, Ideas} AND `answerChosenAt == null` → dedup by searching issue bodies for the discussion URL (`gh search issues` / `gh issue list --search` with the URL, both states) → render a dedicated `Discussions (actionable)` block after the issues table with per-row `→ /idd-issue --from-discussion ` as the suggested next, and a footer count. Zero actionable → one-line note, not silence (surfacing tools must not silently swallow an empty channel). + +### D3 — idd-issue seeding records provenance verbatim; the reply is draft-and-confirm with a hard unattended boundary + +`--from-discussion `: fetch the Discussion, seed the issue body with a `## Provenance` section (Discussion URL + author + a verbatim blockquote of the opening post — blockquote per the plugin's 原文引用 discipline), then continue the normal idd-issue flow (type/priority gathering, privacy gate, egress). After the issue exists: draft the back-reference reply (`Filed as — follow-up there`) and (attended) AskUserQuestion confirm before `gh api graphql` addDiscussionComment; (unattended) **never post** — print the draft in the Step 5 report as "suggested reply (not posted)". This mirrors the #141 surface-only contract for outward-facing writes. + +### D4 — Drift-guard tests mock nothing live + +`scripts/tests/discussions-intake/test.sh` asserts the prose contract (flags documented, constraints named, reference doc sections present, unattended draft-only boundary stated, no-auto-file literal) — the falsifiable form for prose SKILLs, consistent with every sibling suite. No live GraphQL in tests. + +## Risks / Trade-offs + +- **GraphQL schema drift** (Discussions API is younger than REST): the reference doc records the schema assumption (field names + query date); a query failure in the wild degrades to the same one-line skip as discussions-disabled, never a hard abort of idd-list. +- **Dedup search cost**: one search per actionable discussion, bounded by the ≤50 fetch window and the category/answered filters that run first. +- **Opt-in invisibility**: users who never pass `--discussions` keep the blind spot. Accepted for v1 (latency + noise trade-off, per issue body); revisit a config key if the flag proves sticky. + +## Migration Plan + +Additive flags only; no existing invocation changes behavior. No migration. + +## Open Questions + +(none — direction settled in #221 body + diagnosis; Spectra opt-out conditions were met) diff --git a/openspec/changes/discussions-intake-bridge/proposal.md b/openspec/changes/discussions-intake-bridge/proposal.md new file mode 100644 index 0000000..d69fd6d --- /dev/null +++ b/openspec/changes/discussions-intake-bridge/proposal.md @@ -0,0 +1,35 @@ +## Why + +GitHub Discussions are a real intake channel that IDD is completely blind to. On 2026-07-04, `/idd-list` on `PsychQuant/che-ical-mcp` reported a clean backlog (0 open issues) while a real permission-bug report — with the maintainer's diagnosis and the reporter's "it's fixed" confirmation — had lived its entire lifecycle inside Discussion 105. Discussions have no REST API, so `gh issue list` never sees them; every IDD intake surface (`/idd-list` triage, `/idd-issue` filing) is bound to the Issues tab only. (#221) + +## What Changes + +- `/idd-list` gains an **opt-in** `--discussions` flag: fetch open Discussions via `gh api graphql`, no-op gracefully when the repo has Discussions disabled, filter to actionable ones (Q&A / Ideas category, unanswered via `answerChosenAt: null`, not already referenced by an existing issue), and render them in a dedicated block with a suggested next action. Opt-in keeps the default invocation latency- and noise-free. +- `/idd-issue` gains `--from-discussion `: seed the issue body from the Discussion (provenance section with URL + verbatim blockquote of the opening post), and after the issue exists, post a back-reference reply in the Discussion — **draft-and-confirm only**; under unattended mode the reply is drafted but never posted (surfaced in the report instead). +- New reference `plugins/issue-driven-dev/references/discussions-intake.md` holds the normative contract (no-auto-file / dedup / resolution-detection) plus the GraphQL query templates both skills share. +- **Hard constraint carried from the motivating case: the tool never auto-files.** Discussion 105 was already resolved when it was discovered — mechanical filing would have created a noise issue. The bridge surfaces; the human judges. + +## Non-Goals + +- **Latest-comment sentiment analysis** for resolution detection — `answerChosenAt` is the honest mechanical boundary; reading "everything is fixed, thank you" is human judgment (diagnosis Residue, #221). +- **Auto-filing issues from Discussions** — rejected by design (see the che-ical-mcp 105 case). +- **Always-on Discussions fetch in idd-list** — opt-in flag only; no config key in v1 (a config key can be added later if the flag proves sticky). +- **Discussions write-path beyond the single back-reference reply** (no answering, no labeling, no closing Discussions). + +## Capabilities + +### New Capabilities + +- `discussions-intake`: the Discussions→IDD intake bridge contract — idd-list surfacing (opt-in, filtered, deduped), idd-issue seeding with provenance, the no-auto-file / dedup / resolution-detection constraints, and the shared GraphQL templates. + +### Modified Capabilities + +(none — idd-list and idd-issue behavior extensions are additive flags; no existing spec's normative clauses change) + +## Impact + +- Affected specs: new `discussions-intake` +- Affected code: + - New: plugins/issue-driven-dev/references/discussions-intake.md, plugins/issue-driven-dev/scripts/tests/discussions-intake/test.sh + - Modified: plugins/issue-driven-dev/skills/idd-list/SKILL.md, plugins/issue-driven-dev/skills/idd-issue/SKILL.md, plugins/issue-driven-dev/references/usecase-routing.md, plugins/issue-driven-dev/README.md + - Removed: (none) diff --git a/openspec/changes/discussions-intake-bridge/specs/discussions-intake/spec.md b/openspec/changes/discussions-intake-bridge/specs/discussions-intake/spec.md new file mode 100644 index 0000000..64da8f0 --- /dev/null +++ b/openspec/changes/discussions-intake-bridge/specs/discussions-intake/spec.md @@ -0,0 +1,53 @@ +## ADDED Requirements + +### Requirement: idd-list surfaces actionable Discussions on opt-in + +`/idd-list` SHALL fetch open GitHub Discussions via `gh api graphql` when — and only when — the `--discussions` flag is passed. When the repository has Discussions disabled (`hasDiscussionsEnabled: false`), the step SHALL no-op with a single visible note and continue. A Discussion SHALL be surfaced as actionable only when its category is Q&A or Ideas, `answerChosenAt` is null, and no existing issue (open or closed) references its URL. Actionable Discussions SHALL render in a dedicated block with a suggested next action pointing at `/idd-issue --from-discussion `; zero actionable SHALL print a one-line note rather than silence. + +#### Scenario: repo with Discussions disabled + +- **GIVEN** a target repo where `hasDiscussionsEnabled` is false +- **WHEN** the user runs `/idd-list --discussions` +- **THEN** the run prints one skip note for the Discussions step and the issues table renders unaffected + +#### Scenario: answered discussion is not flagged + +- **GIVEN** a Q&A Discussion whose `answerChosenAt` is non-null +- **WHEN** `/idd-list --discussions` runs +- **THEN** that Discussion does not appear in the actionable block + +#### Scenario: discussion already referenced by an issue is deduped + +- **GIVEN** an unanswered Q&A Discussion whose URL appears in the body of an existing closed issue +- **WHEN** `/idd-list --discussions` runs +- **THEN** that Discussion does not appear in the actionable block + +### Requirement: idd-issue seeds an issue from a Discussion with verbatim provenance + +`/idd-issue` SHALL accept `--from-discussion `, fetch the Discussion via GraphQL, and seed the drafted issue body with a Provenance section containing the Discussion URL, its author, and a verbatim blockquote of the opening post. The seeded draft SHALL then flow through the unchanged idd-issue pipeline (type/priority gathering, privacy gate, egress wrapper). + +#### Scenario: seeding from a Discussion URL + +- **GIVEN** an unanswered Q&A Discussion reporting a bug +- **WHEN** the user runs `/idd-issue --from-discussion ` +- **THEN** the drafted body contains a Provenance section with the URL and a blockquote of the opening post, and issue creation proceeds through the normal gates + +### Requirement: the intake bridge never auto-files and never auto-posts + +The bridge SHALL NOT create an issue from a Discussion without an explicit human invocation of `--from-discussion`, and SHALL NOT post the back-reference reply to the Discussion without explicit confirmation. Under unattended mode the reply SHALL be drafted and surfaced in the report but never posted. + +#### Scenario: unattended run drafts but does not post the reply + +- **GIVEN** `/idd-issue --from-discussion ` running under an orchestrator's UNATTENDED MODE directive +- **WHEN** the issue is created successfully +- **THEN** the back-reference reply text appears in the final report marked as not posted, and no `addDiscussionComment` mutation is executed + +### Requirement: the contract and GraphQL templates live in one shared reference + +The normative constraints (no-auto-file, dedup, resolution-detection) and every GraphQL query the bridge uses (hasDiscussionsEnabled probe, discussions list, single-discussion fetch) SHALL be defined once in the discussions-intake reference document; both skills SHALL cite it rather than embedding divergent copies. + +#### Scenario: adding a field to the list query + +- **GIVEN** a maintainer extending the discussions list query with a new field +- **WHEN** they edit the reference document's query template +- **THEN** both idd-list and idd-issue consume the change without any skill-local query text needing a parallel edit diff --git a/openspec/changes/discussions-intake-bridge/tasks.md b/openspec/changes/discussions-intake-bridge/tasks.md new file mode 100644 index 0000000..25230b8 --- /dev/null +++ b/openspec/changes/discussions-intake-bridge/tasks.md @@ -0,0 +1,17 @@ +## 1. Contract + tests first (RED) + +- [x] 1.1 (Design D1; Req: the contract and GraphQL templates live in one shared reference) Write plugins/issue-driven-dev/references/discussions-intake.md — the three normative constraints (no-auto-file / dedup / resolution-detection with `answerChosenAt` as the mechanical boundary), the `hasDiscussionsEnabled` probe query, the discussions list query (first 50: number, title, url, category name, answerChosenAt, updatedAt, author login), the single-discussion fetch query, and the schema-assumption note (field names + date). Verify: file exists and contains all three constraint names + three query blocks. +- [x] 1.2 (Design D4) Write plugins/issue-driven-dev/scripts/tests/discussions-intake/test.sh (assert-helpers): asserts idd-list documents `--discussions` + disabled-repo no-op + category/answered/dedup filter + dedicated block; idd-issue documents `--from-discussion` + Provenance blockquote + draft-and-confirm reply + unattended draft-only; reference doc carries constraints + queries; both skills cite the reference. Run it: RED (SKILL prose absent), reference assertions GREEN. + +## 2. idd-list Step 2.7 (GREEN part 1) + +- [x] 2.1 (Design D2; Req: idd-list surfaces actionable Discussions on opt-in) Add Step 2.7 to plugins/issue-driven-dev/skills/idd-list/SKILL.md: opt-in `--discussions` flag parse (Step 1 table row), probe → skip-note on disabled, fetch → filter (Q&A/Ideas AND answerChosenAt null) → dedup (search issue bodies for the discussion URL, both states) → `Discussions (actionable)` block after the issues table with per-row `→ /idd-issue --from-discussion ` + footer count; zero-actionable one-line note; GraphQL failure degrades to the same skip-note (never aborts idd-list). Bootstrap TaskCreate list gains a `fetch_discussions` entry. Verify: discussions-intake suite idd-list assertions GREEN. + +## 3. idd-issue --from-discussion (GREEN part 2) + +- [x] 3.1 (Design D3; Req: idd-issue seeds an issue from a Discussion with verbatim provenance; Req: the intake bridge never auto-files and never auto-posts) Add `--from-discussion ` to plugins/issue-driven-dev/skills/idd-issue/SKILL.md: fetch via the reference's single-discussion query, seed draft body with `## Provenance` (URL + author + verbatim blockquote of opening post), continue unchanged pipeline; after creation, draft back-reference reply — attended: AskUserQuestion confirm then `addDiscussionComment`; unattended: print draft in Step 5 report marked not-posted, never mutate. Bootstrap TaskCreate list gains a `seed_from_discussion` entry. Verify: discussions-intake suite idd-issue assertions GREEN. + +## 4. Docs + full-suite regression + +- [x] 4.1 Add the two flags to plugins/issue-driven-dev/references/usecase-routing.md (one new scenario row: "user report arrived as a Discussion") and a short README section under the skills table noting the opt-in Discussions bridge (#221). Verify: discussions-intake suite doc assertions GREEN. +- [x] 4.2 Run the full plugin test set (all suites under plugins/issue-driven-dev/scripts/tests/) and `spectra validate discussions-intake-bridge`. Verify: no suite regresses; validate clean. diff --git a/plugins/issue-driven-dev/README.md b/plugins/issue-driven-dev/README.md index af622c9..13cc209 100644 --- a/plugins/issue-driven-dev/README.md +++ b/plugins/issue-driven-dev/README.md @@ -207,6 +207,10 @@ attachments_release: "attachments" --- ``` +### Discussions intake bridge(v2.95.0, #221) + +GitHub Discussions(Q&A / Ideas)是 Issues 之外的真實 intake channel — 本 plugin 現在能看到它:`idd-list --discussions`(**opt-in**)surface actionable 的 Discussions(未答 `answerChosenAt: null`、未被任何 issue 引用、Discussions 停用時優雅 no-op),`idd-issue --from-discussion ` 把一則 Discussion seed 成 issue(`## Provenance` + 原文 verbatim blockquote),建案後的 back-reference 回文 **draft-and-confirm**(unattended 絕不 auto-post)。**鐵律:絕不 auto-file** — bridge 只 surface,人來判斷。契約 + GraphQL 樣板:[references/discussions-intake.md](references/discussions-intake.md)。 + ### Selective auto-tag(v2.94.0, #85) IDD tags two milestones so you can jump back to a stable checkpoint with `git checkout` instead of hunting commit hashes: `idd-{N}-baseline` (placed by `idd-issue` on main's HEAD when the issue opens — the rollback anchor) and `idd-{N}-verified` (placed by `idd-verify` on the Aggregate-PASS snapshot — review-ready). Only these two points are tagged (no `diagnose`/`plan`/`implement` tags), so the tag namespace stays clean. diff --git a/plugins/issue-driven-dev/references/discussions-intake.md b/plugins/issue-driven-dev/references/discussions-intake.md new file mode 100644 index 0000000..1c908e0 --- /dev/null +++ b/plugins/issue-driven-dev/references/discussions-intake.md @@ -0,0 +1,108 @@ +# Discussions Intake Bridge(#221) + +GitHub **Discussions** 是真實 intake channel(bug report / feature request 常以 Q&A 形式抵達), +但 Discussions **沒有 REST API** — `gh issue list` 天然看不到。本檔是 `idd-list --discussions` 與 +`idd-issue --from-discussion` 共用的 **normative contract + GraphQL 樣板**(single source;兩個 +skill 引用本檔,不得各自內嵌分歧的 query 副本)。 + +動機案例:2026-07-04 `/idd-list` 對 `PsychQuant/che-ical-mcp` 回報 0 open issues(乾淨 backlog), +但一條真實權限 bug 的完整生命週期(回報 → 診斷 → 「已修好」確認)都發生在 Discussion 105 — +IDD 對整條 intake channel 全盲。 + +## Normative constraints(三條,缺一不可) + +### 1. no-auto-file — 絕不自動建 issue + +Bridge **只 surface,人來判斷**。動機案例的 Discussion 被發現時**已解決**(回報者最後一則留言 +是「everything is fixed, thank you」)— 機械式對每個「問題貌」Discussion 建 issue 會製造 +開了就要關的 noise issue。`--from-discussion` 是**顯式人為呼叫**,不是自動化觸發點。 + +### 2. dedup — 已被 issue 引用者不重複標記 + +任何**既有 issue(open 或 closed)** body 內含該 Discussion URL → 該 Discussion 不列入 +actionable。搜尋方式:`gh issue list --repo --state all --search "" --json number` +非空即視為已橋接。 + +### 3. resolution-detection — `answerChosenAt` 是機械邊界 + +`answerChosenAt != null`(已選答案)→ 不 actionable。**留言情緒判讀(「已修好,謝謝」)明文 +out of scope** — 那是人的判斷,不是機械信號(#221 diagnosis Residue)。category 過濾:只有 +**Q&A 與 Ideas** 算 intake;Announcements / Show-and-tell / Polls 不算。 + +## Outward-write 邊界(idd-issue 端) + +建案後的 Discussion 回文(back-reference reply)**一律 draft-and-confirm**: +- **attended**:draft 呈現 → `AskUserQuestion` 確認 → 才執行 `addDiscussionComment` mutation +- **unattended**(orchestrator UNATTENDED MODE):**draft-only 絕不 post** — 回文草稿印在 + Step 5 report 標記「suggested reply (not posted)」 + +## GraphQL 樣板 + +> **Schema 假設(2026-07-11 驗於 GitHub GraphQL v4)**:`Repository.hasDiscussionsEnabled`、 +> `Repository.discussions(first, states)`、`Discussion.{number,title,url,body,createdAt,updatedAt,answerChosenAt}`、 +> `Discussion.category.name`、`Discussion.author.login`、mutation `addDiscussionComment(discussionId, body)`。 +> Discussions API 比 REST 年輕 — query 失敗時**降級為一行 skip note,絕不 hard-abort 呼叫端 skill**。 + +### Probe — hasDiscussionsEnabled + +```bash +gh api graphql -f query='query($owner:String!,$repo:String!){ + repository(owner:$owner,name:$repo){ hasDiscussionsEnabled } +}' -f owner="$OWNER" -f repo="$REPO_NAME" --jq '.data.repository.hasDiscussionsEnabled' +``` + +### List — open discussions(first 50) + +```bash +gh api graphql -f query='query($owner:String!,$repo:String!){ + repository(owner:$owner,name:$repo){ + discussions(first:50, states:OPEN, orderBy:{field:UPDATED_AT, direction:DESC}){ + nodes{ number title url updatedAt answerChosenAt + category{ name } author{ login } } + } + } +}' -f owner="$OWNER" -f repo="$REPO_NAME" \ + --jq '.data.repository.discussions.nodes[]' +``` + +Actionable filter(呼叫端套用,順序固定 — 便宜的先): +`category.name ∈ {"Q&A","Ideas"}` → `answerChosenAt == null` → dedup search(constraint 2)。 + +### Fetch one — for `--from-discussion` seeding + +```bash +gh api graphql -f query='query($owner:String!,$repo:String!,$num:Int!){ + repository(owner:$owner,name:$repo){ + discussion(number:$num){ id number title url body createdAt answerChosenAt + category{ name } author{ login } } + } +}' -f owner="$OWNER" -f repo="$REPO_NAME" -F num="$DISCUSSION_NUM" \ + --jq '.data.repository.discussion' +``` + +(`id` 是 node ID — 之後 `addDiscussionComment` 的 `discussionId` 用它。) + +### Reply — back-reference(attended confirm 後才執行) + +```bash +gh api graphql -f query='mutation($id:ID!,$body:String!){ + addDiscussionComment(input:{discussionId:$id, body:$body}){ comment { url } } +}' -f id="$DISCUSSION_NODE_ID" -f body="$REPLY_BODY" +``` + +## Provenance 格式(idd-issue seeding) + +```markdown +## Provenance + +> Source: (Discussion #,author @,category ) + +原文(opening post,verbatim blockquote): + +> +``` + +原文一律 blockquote(plugin 原文引用紀律);issue body 其餘段落照 idd-issue 正常 pipeline +(type / priority / privacy gate / gh-egress)。 + +Refs #221。 diff --git a/plugins/issue-driven-dev/references/usecase-routing.md b/plugins/issue-driven-dev/references/usecase-routing.md index 6a4b194..015bded 100644 --- a/plugins/issue-driven-dev/references/usecase-routing.md +++ b/plugins/issue-driven-dev/references/usecase-routing.md @@ -66,6 +66,7 @@ | **25** | Emergent multi-issue solve from one root — root issue 解到一半 spawn 出 sister bug / follow-up,想全鏈在 1 個 PR review(v2.55.0+ single-root, v2.60.0+ multi-root + DFS/BFS) | `idd-all-chain #N` 或 `idd-all-chain #A #B #C [--bfs]` | (無 — 內部用 `--in-chain` 自動推導 4th mode tuple `(direct-commit, unattended)`);hard caps per-root depth=3 / global max-issues=10 (v2.60.0+, was 2/5);eligibility = same-file OR same-skill OR sister-bug;multi-root 時 verify FAIL = per-root halt(其他 root subtree 繼續)| [chain-flow.md](chain-flow.md), [spawn-manifest.md](spawn-manifest.md) | | **26** | Multi-finding source(transcript / docx / 老師回饋)— 一個 source 含 ≥2 findings 要分流到 mixed routing(部分 new issue、部分 amend 既存 #N comment / edit / update),用一次 invocation 取代手敲 N 次 atomic skill(v2.56.0+)| `idd-issue source.docx`(auto-trigger when ≥2 findings)或 `idd-issue --multi-finding "text"`(force in)| `--multi-finding` / `--no-multi-finding`(override auto-detect);**互斥**`--bundle-mode`;Stage 2 picker AI surface top-3 candidates 不 decide;Stage 3 batch preview confirm;Stage 4 warn-continue dispatch + per-action footer + jsonl run log at `.claude/.idd/issue-runs/.jsonl`(commit 進 git)| `skills/idd-issue/SKILL.md` `## Multi-finding source mode` 段落 | | **27** | N 個 unrelated open issues 想一次解(無 emergent 關係,backlog drain)| **`idd-all #A #B #C`**(v2.83.0+ multi-issue batch mode)— conflict-class-**ordered sequential** drain:依 `### Conflict Class` 排序(`E`/`D` 先、同資源 `B`/`C` 相鄰序列化、同檔成組、`A` 不拘),逐一跑完整 idd-all pipeline | (無 — 每 issue 讀 `### Conflict Class`,缺則預設 `D_diagnose_first` surfaced);**sequential**(真並發 stateful lane = deferred,見 worktree-isolation Case A);`A` 可選 worktree 隔離供事後手動並行;stop at verified | [parallel-orchestration.md](parallel-orchestration.md)(取代先前「無內建 bulk-solve」註記;conflict-class taxonomy = 並行安全契約;參 parked [#37](https://github.com/PsychQuant/issue-driven-development/issues/37))| +| **28** | 使用者回報以 **GitHub Discussion** 形式抵達(Q&A / Ideas),要納入 IDD intake | `idd-list --discussions`(surface actionable)→ 人判斷 → `idd-issue --from-discussion `(seed + provenance + draft-and-confirm 回文)| `--discussions` / `--from-discussion` | [discussions-intake.md](discussions-intake.md) | > **Chain-solve vs bulk-solve semantic 差異**(per #49 cross-link): > - **`/idd-all-chain #N`**(v2.55.0+, row 25, single-root):**hot-chain from active work** — 1 root + auto-emergent spawn(sister bug / verify follow-up / mid-plan tangential / sister concern detected via spawn manifest)。Bounded by hard caps per-root depth=3、global max-issues=10 (v2.60.0+, was 2/5)。Reviewer mental model:「this thread is complete」。 diff --git a/plugins/issue-driven-dev/scripts/tests/discussions-intake/test.sh b/plugins/issue-driven-dev/scripts/tests/discussions-intake/test.sh new file mode 100755 index 0000000..d4af242 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/discussions-intake/test.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# test.sh — drift-guard for the #221 Discussions intake bridge +# (change discussions-intake-bridge, spec discussions-intake). +# +# The bridge spans TWO prose SKILLs + ONE shared reference that MUST agree: +# - references/discussions-intake.md (contract source of truth + GraphQL) +# - skills/idd-list/SKILL.md (--discussions surfacing, Step 2.7) +# - skills/idd-issue/SKILL.md (--from-discussion seeding + reply) +# The real failure mode is DRIFT (change the contract in one file, forget the +# others) plus EROSION of the three normative constraints — especially +# no-auto-file, whose violation recreates the exact noise the motivating +# case (che-ical-mcp discussion 105) proved harmful. +# +# Usage: bash test.sh (exit 0 = all pass, 1 = any fail) + +set -u + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PLUGIN_ROOT="$HERE/../../.." +REF="$PLUGIN_ROOT/references/discussions-intake.md" +LIST="$PLUGIN_ROOT/skills/idd-list/SKILL.md" +ISSUE="$PLUGIN_ROOT/skills/idd-issue/SKILL.md" + +HELPERS="$HERE/../../lib/assert-helpers.sh" +[ -f "$HELPERS" ] || { echo "✗ missing $HELPERS — cannot run suite" >&2; exit 1; } +. "$HELPERS" + +# ── reference: the contract + all three queries live here, once ── +assert_file_exists "shared reference exists" "$REF" +assert_output_grep "ref: no-auto-file constraint" "no-auto-file" "$REF" +assert_output_grep "ref: dedup constraint" "dedup" "$REF" +assert_output_grep "ref: resolution-detection constraint" "resolution-detection" "$REF" +assert_output_grep "ref: answerChosenAt mechanical boundary" "answerChosenAt" "$REF" +assert_output_grep "ref: hasDiscussionsEnabled probe query" "hasDiscussionsEnabled" "$REF" +assert_output_grep "ref: list query (first 50)" "discussions(first:50" "$REF" +assert_output_grep "ref: single-discussion fetch query" "discussion(number:" "$REF" +assert_output_grep "ref: reply mutation (confirm-gated)" "addDiscussionComment" "$REF" +assert_output_grep "ref: schema assumption note dated" "Schema 假設" "$REF" +assert_output_grep "ref: unattended draft-only boundary" "draft-only 絕不 post" "$REF" + +# ── idd-list: opt-in surfacing (Step 2.7) ── +assert_output_grep "idd-list: --discussions flag documented" "--discussions" "$LIST" +assert_output_grep "idd-list: Step 2.7 section present" "Step 2.7" "$LIST" +assert_output_grep "idd-list: disabled-repo graceful skip" "hasDiscussionsEnabled" "$LIST" +assert_output_grep "idd-list: category filter (Q&A/Ideas)" "Q&A" "$LIST" +assert_output_grep "idd-list: unanswered signal" "answerChosenAt" "$LIST" +assert_output_grep "idd-list: dedup against issue refs" "dedup" "$LIST" +assert_output_grep "idd-list: dedicated actionable block" "Discussions (actionable)" "$LIST" +assert_output_grep "idd-list: suggested next → from-discussion" "idd-issue --from-discussion" "$LIST" +assert_output_grep "idd-list: bootstrap task fetch_discussions" "fetch_discussions" "$LIST" +assert_output_grep "idd-list: cites the shared reference" "discussions-intake" "$LIST" + +# ── idd-issue: --from-discussion seeding + confirm-gated reply ── +assert_output_grep "idd-issue: --from-discussion flag" "--from-discussion" "$ISSUE" +assert_output_grep "idd-issue: Provenance section seeded" "## Provenance" "$ISSUE" +assert_output_grep "idd-issue: verbatim blockquote of opening post" "verbatim blockquote" "$ISSUE" +assert_output_grep "idd-issue: reply is draft-and-confirm" "draft-and-confirm" "$ISSUE" +assert_output_grep "idd-issue: unattended never posts reply" "suggested reply (not posted)" "$ISSUE" +assert_output_grep "idd-issue: bootstrap task seed_from_discussion" "seed_from_discussion" "$ISSUE" +assert_output_grep "idd-issue: cites the shared reference" "discussions-intake" "$ISSUE" + +# ── the cardinal rule survives in BOTH consumer skills ── +assert_output_grep "idd-list: never auto-files" "絕不自動建 issue" "$LIST" +assert_output_grep "idd-issue: never auto-posts the reply" "絕不 auto-post" "$ISSUE" + +print_summary "discussions-intake (#221)" diff --git a/plugins/issue-driven-dev/skills/idd-issue/SKILL.md b/plugins/issue-driven-dev/skills/idd-issue/SKILL.md index 9e8c4ed..35328dc 100644 --- a/plugins/issue-driven-dev/skills/idd-issue/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-issue/SKILL.md @@ -4,7 +4,7 @@ description: | 建立 well-documented GitHub Issue。每個改動的起點。 Use when: 報 bug、追蹤需求、任何需要正式記錄的工作。 防止的失敗:改了東西卻沒有文件記錄「為什麼改」。 -argument-hint: "[description or path to .docx] [--target owner/repo]" +argument-hint: "[description or path to .docx] [--target owner/repo] [--from-discussion url|number]" allowed-tools: - Bash(gh:*) - Bash(cp:*) @@ -100,6 +100,7 @@ Fork 有兩種相反的使用情境: ``` TaskCreate(name="detect_target_repo", description="Step 0.5: 解析 target — --target flag → walked-up config → predicate pre-resolve → fork/third-party detection (順序: fork E2 → third-party E-TP → E1)") TaskCreate(name="read_source", description="讀取來源(docx → mcp__che-word-mcp 讀文字 + 列圖片)") +TaskCreate(name="seed_from_discussion", description="Step 1.7 (v2.95+, #221): 僅當 --from-discussion — GraphQL 讀 discussion(含 node id)→ 以 ## Provenance(URL + author + opening post verbatim blockquote)seed 草稿 body → 走正常 pipeline;建案後 back-reference 回文 draft-and-confirm(unattended = draft-only 絕不 post,印於 Step 5 report)") TaskCreate(name="gather_info", description="Step 2: 蒐集 title / type / priority / description") TaskCreate(name="reresolve_target", description="Step 2.5: 用 title/labels 重評 content predicates,若新匹配 != tentative_default 則問使用者要不要切") TaskCreate(name="resolve_mentions", description="若有 --mention 或 description 含 @xxx,強制走 rules/tagging-collaborators.md 協定(v2.32.0+)") @@ -520,6 +521,18 @@ echo "" echo " 3) 全部存好後告訴我「ok」,skill 會接手 upload + 嵌入 issue body" ``` +### Step 1.7: Seed from a Discussion(`--from-discussion`,v2.95+,#221) + +**僅當 `--from-discussion ` 存在**才執行。GitHub Discussions 是 Issues 之外的真實 intake channel;本 step 把一則 Discussion 轉成 well-documented issue 的**種子**,但**絕不自動建 issue** — flag 本身就是顯式人為呼叫(no-auto-file 契約)。契約 + GraphQL 的 single source 是 [`references/discussions-intake.md`](../../references/discussions-intake.md)。 + +1. **Fetch**:以 reference 的 single-discussion query 讀 title / body / url / author / category / `answerChosenAt` / node `id`(回文用) +2. **已答警示**:`answerChosenAt != null` → 印 warning「此 discussion 已有選定答案(可能已解決)」但**不擋**(人已顯式指名,人判斷) +3. **Seed 草稿 body**:開頭插入 `## Provenance` 段 — Discussion URL + author + category + **opening post 的 verbatim blockquote**(原文引用紀律:逐行 `>`);title 預設沿用 discussion title(Step 2 可改) +4. **正常 pipeline**:其餘照 Step 2 起流程走(type / priority / privacy gate / gh-egress)— seeding 不繞過任何既有 gate +5. **Back-reference 回文(建案成功後)**:draft 回文 `Filed as — follow-up there.`,**draft-and-confirm**: + - **attended**:AskUserQuestion 確認 → 才跑 reference 的 `addDiscussionComment` mutation + - **unattended**:**絕不 auto-post** — 草稿印在 Step 5 report 標 `suggested reply (not posted)`,人事後自行貼 + ### Step 2: 蒐集資訊 缺少的話詢問使用者: diff --git a/plugins/issue-driven-dev/skills/idd-list/SKILL.md b/plugins/issue-driven-dev/skills/idd-list/SKILL.md index 4822899..e051b0c 100644 --- a/plugins/issue-driven-dev/skills/idd-list/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-list/SKILL.md @@ -45,6 +45,7 @@ allowed-tools: TaskCreate(name="parse_args", description="Parse --state / --label / --limit / --repo flags 並 fallback 到 .claude/issue-driven-dev.local.json") TaskCreate(name="fetch_issues", description="gh issue list 取 number/title/state/labels/updatedAt/body/comments") TaskCreate(name="fetch_open_prs", description="Step 2.5 (v2.51+): gh pr list --state open --json number,body,title,isDraft,mergeable,headRefName,createdAt --limit 100 一次抓所有 open PR") +TaskCreate(name="fetch_discussions", description="Step 2.7 (v2.95+, #221): 僅當 --discussions flag — probe hasDiscussionsEnabled(false → 一行 skip note)→ GraphQL 抓 open discussions(first 50)→ filter(Q&A/Ideas ∧ answerChosenAt null)→ dedup(issue body 含該 URL 者剔除)→ Discussions (actionable) 區塊。query 失敗降級 skip note,絕不 abort。無 flag 時 no-op") TaskCreate(name="extract_phase", description="從每個 issue body 的 Current Status → **Phase**: 抽出 phase;fallback 掃 comments 標題推斷") TaskCreate(name="build_issue_pr_index", description="Step 3.5 (v2.51+): client-side regex `#(\d{1,7})\b` scan PR body 找 issue refs (cap digits ≤7,過濾 #0),反向建 issue→PR map + cluster detection (refs ≥ 2 → cluster,leader = min(refs);若同 issue 被多 PR ref,sort by PR number asc 確保 deterministic order;cluster_members 寫入 pr_info 僅當 len ≥ 2)") TaskCreate(name="extract_blocked_state", description="Step 3.7 (v2.92+, #84): 抽 blocked 信號(Blocking 區塊/label/wait 類 next)→ blocked_reason 掛 entry") @@ -66,6 +67,7 @@ TaskCreate(name="audit_closes_marker", description="Step 4 (v2.75.2+, #151): 若 | `--limit` | `20` | 最多顯示筆數 | | `--repo` | _(from config)_ | 覆寫 config 的 repo | | `--audit-closes` | off | 旗標:標記 **CLOSED 但無 `## Closing Summary`** 的 issue(可能被 commit / PR-body close keyword auto-close 繞過 `/idd-close` gate,#151)。`--state` 仍是預設 `open` 時隱含切到 `closed`。底層 primitive:`scripts/check-closed-without-summary.sh`(standalone / cron 可直接呼叫)| +| `--discussions` | off | **Opt-in**(#221):同場 surface GitHub Discussions 的 actionable 項(Q&A/Ideas、未答、未被任何 issue 引用)。契約 + GraphQL 見 [`references/discussions-intake.md`](../../references/discussions-intake.md) | ### Step 2: Fetch Issues @@ -98,6 +100,20 @@ gh pr list \ **為何只看 open PR**:list 的目的是「actionable next step」,closed/merged PR 對應 issue 應已 close 或 catch-up close,本 step 不重複處理。 +### Step 2.7: Fetch Discussions(opt-in,v2.95+,#221) + +**僅當 `--discussions` flag 存在**才執行;無 flag 完全 no-op(預設 invocation 零延遲、零噪音)。Discussions 是 IDD 全盲的 intake channel(動機案例:che-ical-mcp 的權限 bug 整條生命週期都在 Discussion 105,`/idd-list` 回報 0 open issues)。契約、GraphQL 樣板、schema 假設的 single source 是 [`references/discussions-intake.md`](../../references/discussions-intake.md) — 本 step 引用它,**不**內嵌分歧的 query 副本。 + +流程(依 reference 的 actionable filter 順序 — 便宜的先): + +1. **Probe**:`hasDiscussionsEnabled` query — `false` → 印一行 `(discussions disabled on this repo — skip)` 繼續;query 失敗(GraphQL schema 漂移 / 網路)→ 同樣降級為一行 skip note,**絕不 abort idd-list** +2. **Fetch**:open discussions(first 50,UPDATED_AT desc;欄位:number / title / url / updatedAt / answerChosenAt / category.name / author.login) +3. **Filter**:`category.name ∈ {"Q&A","Ideas"}` **且** `answerChosenAt == null`(已答 = 已解決,不 actionable — 留言情緒判讀明文不做) +4. **Dedup**:對每個候選跑 `gh issue list --state all --search ""` — 任何既有 issue(open 或 closed)引用該 URL → 剔除(已橋接) +5. **Render**:issues 表格之後加 `Discussions (actionable)` 區塊,每列含 title / category / updated + suggested next `→ /idd-issue --from-discussion `;footer 統計加 discussions 計數。**零 actionable → 一行 note(不沉默)**:`(discussions: 0 actionable of N open)` + +**鐵律(no-auto-file)**:本 step **絕不自動建 issue** — surface + 人判斷。機械式對每個「問題貌」Discussion 建案會製造開了就關的 noise(reference 檔 constraint 1 的動機案例)。 + ### Step 3: Extract Phase 每個 issue 的 body 由 `idd-update` 管理的 `## Current Status` 區塊含 `**Phase**: {phase}` 行。優先從這裡讀。