diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index b2dc3b8..1853a39 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -15,7 +15,7 @@ "plugins": [ { "name": "issue-driven-dev", - "version": "2.103.1", + "version": "2.103.2", "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. v2.102.1: reopen / resume path (#278) — the legal return trip from closed. idd-close gains a 'Reopen / resume path' section (close's dual operation): reopen-vs-new-issue criteria (same Expected -> reopen for trail continuity; morphed need -> new issue Refs old; broken upstream artifact -> #200's re-baseline, out of this path), resume point decided by the closing summary's WHY (premise changed -> re-diagnose; pure deferral -> implement), and the old summary stays untouched (append-only; reopen = note comment + idd-update phase rollback + optional prepend-note). Cross-referenced against auto-close-trap recovery. usecase-routing scenario 31. From a real user exchange; verify on substitute basis (disclosed). v2.102.0: three-front release. Skill-description contract + Path Map (#276, two-phase): idd-plan's frontmatter description — the ONLY surface read at skill-selection time — now names its diagnosis precondition; 5 skills gain the house pattern (drift-guard skill-description-contract, RED 8 first); docs/workflows.md gains a mermaid Path Flowchart mirroring the decision tree with all 36 catalog paths, rendered deterministically to the wiki Path-Map page by scripts/generate-path-map.py (drift-guard path-map-sync: freshness / coverage / discovery). Egress data-safety cluster (#275 + #273): empty-body guard — a provided-but-empty body now refuses (exit 15, band discipline; edit floors at 10 stripped chars because overwrite semantics turn empty dispatch into data loss — live incident 2026-07-22; explicit-intent escape --allow-empty-body); and the comment-PATCH surgery channel enters the nets via the new edit-comment verb (the #226 rollout's tracked-separately whitelist debt retired — it had bypassed EVERY net), with idd-edit's batch loop consuming the refusal band into a second outcome bucket (final exit stays 4). Dogfood: the #163 contract layer caught this release's own SCRUB_LEVEL provenance gap on first sweep. 42 suites, 0 fail. v2.99.1: staleness sweep + guard-net expansion (#267). README carried three stale gpt-5.5 pins and a stale vendored-codex-call claim — all outside the drift-guard scan net; fixed and the net widened: model-generation-sync now refutes pins in README + both catalog docs (31 assertions), and a new docs-catalog-sync suite requires every skills/* directory to appear in the catalog docs (the #122 no-forcing-function root cause is now test-detectable; it caught idd-ask and idd-config on its first RED). docs/workflows.md + skill-dimensions.md backfilled to v2.99 reality (P-find-lookup / P-ask-history / P-report-rollup / P-config-maintain / P-verify-file-profile paths, matrix rows, D12 4th member). 38 suites 0 fail. v2.99.0: /idd-ask — grounded QA over the issue corpus (#72), the surfacing family's 4th member mirroring /spectra-ask. Natural-language question -> decide-to-search gate (greetings/meta skip; bug-shaped questions never trigger diagnose) -> retrieval delegating idd-find's search backend (family rule: never rebuild a read-only query) -> full-text read of top-N hits (default 5, capped 10) -> grounded synthesis: first line blockquotes the question, every claim carries an issue/comment citation, source priority closed-with-PR > open > orphaned comment with conflicts surfaced, ending with Referenced Issues; corpus silence reported honestly, never filled from training memory. Read-only allowed-tools locked. First live run of the #140 fourth-member procedure (Q3 weak-hit judgment recorded in the family canonical). New capability spec idd-ask (+2 requirements); new drift-guard suite; 37 suites 0 fail. v2.98.0: codex channel goes full-dependency (#264, user ruling 'like superpowers'). The vendored bin/codex-call is DELETED — it trailed pai 2.18.0 by four security/correctness fixes (token-exp NSNumber parse, OAuth-file umask 0o077, form-encoding escape, post-flock re-read). Executable now resolves from the parallel-ai-agents plugin cache (MIN_PAI 2.19.0 — the codexModel/codexEffort contract floor, pai issue 22); model/effort/max-time governance resolves from codex-pro's EXTERNAL-CONSUMER CONTRACT (MIN_CODEX_PRO 0.7.0: machine-readable references/defaults.json base + global/project profile.yaml overlay, codex-pro issue 7) and is passed explicitly on all three call paths (canonical Workflow args + manual fan-out + legacy direct). IDD's tree contains ZERO model pins — generation bumps touch codex-pro's defaults.json only. Dependency wiring mirrors the superpowers shape: install-time dependencies entry (codex-pro@codex-pro), allowCrossMarketplaceDependenciesOn, check-plugin-presence pre-flight, fail-fast with a one-step install instruction, no soft fallback. model-generation-sync drift-guard reshaped to the v2 contract (a re-vendored codex-call fails the suite). 36 suites 0 fail. v2.97.0: 9-issue drain via 5 cluster PRs (#259-#263). Composable verification profiles (#258): idd-verify --profile code|prose|academic (+ config-registered custom via verify_profiles) switches the (lens set, DA focus, input source, freshness) four-tuple; new --file/--dir input sources make the git worktree optional; file-mode SHA-256 freshness gate mirrors the #228 diff gate (never silently exempted); code default byte-identical. New /idd-find skill (#139): surfacing-only semantic lookup over the open+closed corpus with GitHub relevance + phase/PR overlay; read-only, filter flags redirect to idd-list, embedding honestly deferred. Dashboard comment contract (#133) + idd-report --rollup (#134): one human-facing narrative snapshot per issue (marker-located, updates bound to phase transitions only, anti-#116) and a pull-only four-group attention view (need-attention / in-progress / stalled>14d / recently-closed). sdd_bias config switch (#252): hard-gate hits escalate to Spectra when high; default routing byte-identical. Layer V unattended deferred-record (#120): registry literal + structured catch-up record aggregated by idd-all Phase 6. Surfacing-primitives family doc, D12 axis (#140). Model-generation sync (#251): codex-call default gpt-5.6-sol is the tree's single generation pin (live-probed); prose generation-neutral; idd-route candidate renamed codex-xhigh. Docs path catalog completed (#122). 5 new drift-guard suites; 36 suites 0 fail. v2.96.0: gh-egress hardening cluster + idd-edit batch semantics. Exit-code band >=10 (#227: 10=privacy/11=mention/12=unscannable/13=attestation/14=usage; wrapper never exits <10 on its own — rc<10 is always gh's, so unattended callers can split gate-refusal from gh-failure on $? alone). Unified python3 content-net scan (#225: kills the jq/no-jq divergence; taxonomy = projects keys + path-shaped values under sensitive key names; fail-closed wide net when python3 absent). Phase 2 rollout (#226: all 6 skills' comment/edit egress now dispatch through gh-egress with attestation — the #117 mention net is mechanically enforced on the comment channel). idd-edit batch x R5 (#158: per-comment refuse + continue, batch outcome report, exit 4 iff any refused). v2.95.0: Discussions intake bridge (#221) — opt-in `idd-list --discussions` (GraphQL surface: Q&A/Ideas + unanswered + deduped vs issue refs; graceful no-op) + `idd-issue --from-discussion` (Provenance seed + draft-and-confirm reply, unattended never posts); cardinal rule: never auto-file. Plus idd-verify diff-freshness gate (#228: FROZEN_SHA vs HEAD before aggregate — refuse stale-snapshot verdicts) and the IDD_CALLER registry (#161: dynamic tree-sweep drift-guard). v2.94.0: selective git auto-tag (#85) — idd-issue tags idd-{N}-baseline at main HEAD (rollback anchor); idd-verify tags idd-{N}-verified on Aggregate PASS (review snapshot). Only these two milestones (no diagnose/plan/implement tags) so the tag namespace stays clean. Config `auto_tag` (default-ON, opt-out via enabled:false); idempotent (existing tag skipped) + graceful-skip on push failure (never aborts the workflow). v2.93.1: collaborator identity registry in idd-config (#86) — optional `collaborators[]` config field mapping a person's alias / email / display-name → GitHub @login WITHOUT guessing (github_login required; email is PII, private/gitignored only). tagging-collaborators.md Step 2.5 consults the registry first as an accelerator (a hit is still existence-verified via `gh api users/`; a miss falls through to the API fuzzy-match); idd-config validate checks login charset + globally-unique aliases + PII reminder. v2.93.0: reshape Plan / pre-implementation tier (Cluster C, #129/#57/#111, via reshape-plan-preimpl-tier Spectra change) — first-class `meeting` issue type (meeting-first routing + Phase A/B/C deliberation + self-contained close gate), complexity hard gate (>=5-file interdependent-concept OR shared-abstraction MUST-trigger Plan, escalate-only), and superpowers pre-implementation hand-off (README stage-mapping table + non-binding brainstorming pointer, no self-built staging skill). v2.92.1: hotfix — parallel-ai-agents install-time dependency pointed at the wrong marketplace (psychquant-claude-plugins), making v2.92.0 fail to load and silently dropping all /idd-* skills; corrected to the parallel-ai-agents marketplace. v2.92.0: /idd-all batch-drain release — 23 issues verified+closed via 16 PRs (#223, #229-#243), the plugin's largest self-dogfood. Added: unattended-contract (state-file signal + TTL, TTY heuristic removed, idd-all/chain dependency early gates #123/#222/#211); gh-egress unconditional @-mention net with --mention-attested escape-or-attest contract (#117) atop 6-item mechanical-net precision hardening (#203); idd-close Step 6.3 doc-sync sweep (#220); test aggregator + GitHub Actions CI, 21 suites (#217); idd-list blocked-state grouping + all-blocked banner (#84); config Mechanism 3.5 submodule routing (#162); check-plugin-presence enabled-state detection exit 3 (#212); monorepo host plugin disambiguation (#68); assert-helpers eval-content ban + safe output-grep pair (#188); diagnosis-detection contract fixtures (#61). Changed: parallel-ai-agents promoted to install-time dependency, vendored ensemble fork DELETED, idd-verify two-tier chain (#219); DA sequenced-spawn eliminates the #119 socket-crash polling window (#130); spectra-archive-post-ic --force-linked-issue vs --linked-issue intent separation (#172); worktree conventions unified on the managed helper (#169); bridge state migrated to .claude/.idd/state/bridge.json (#199); .gitattributes LF policy (#216); merge-completeness fixtures default-branch self-sufficiency (#224). Audits: dependency bindings vs deep-integration rule (#210), rules layering 12/12 (#215). Follow-ups filed: #225-#228.", "author": { "name": "Che Cheng" diff --git a/plugins/issue-driven-dev/CHANGELOG.md b/plugins/issue-driven-dev/CHANGELOG.md index 9614d1a..dc392f1 100644 --- a/plugins/issue-driven-dev/CHANGELOG.md +++ b/plugins/issue-driven-dev/CHANGELOG.md @@ -48,6 +48,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 carried the same blind spot, and all three of that round's new mechanisms had zero test weight. Hence the three rules: third-party surfacing of design shape, a positive control for every guard, and "I enumerated exhaustively" is not evidence. +## [2.103.2] - 2026-08-14 + +### Fixed + +- **`process-attachments.sh` could not see the attachments this plugin itself uploads (#284)** — `idd-issue` Step 4 + uploads to an `attachments` release and writes a `github.com/{o}/{r}/releases/download/{tag}/{file}` URL into the + issue body; the URL detector had no pattern for that shape, so `idd-diagnose` Step 1.5 reported + 「Issue #N has no attachments (empty manifest written)」 and proceeded without the evidence it was supposed to read. + The write side and the read side of one plugin disagreed about a URL shape, and the disagreement was **silent** — an + empty manifest is indistinguishable from an issue that genuinely has none. (Filed three times as #284/#285/#287; + consolidated here.) The URL character classes were also tightened to exclude whitespace: a bare URL in prose used to + swallow the rest of the line. + +- **Non-ASCII attachment names collided silently, and `--clobber` overwrote the loser (#286)** — GitHub sanitizes + release asset filenames by **stripping non-ASCII characters**. Measured: `issue_10_tpa2026_年會議程.pdf` and + `issue_10_nstc_心理學門成果發表會議程.pdf` came back as `issue_10_tpa2026_.pdf` and `issue_10_nstc_.pdf` — the Chinese + segment gone entirely. That instance survived only because the two ASCII prefixes happened to differ. A description + written *entirely* in Chinese — the natural thing for this plugin's own users — sanitizes to the same name for + different files, and with `--clobber` the second upload silently replaces the first: two links in the issue body, one + file behind them, no error anywhere. Names are now ASCII-folded before upload, an all-folded description falls back to + an indexed name rather than an empty one, and a pre-existing asset of the same name causes a rename with a note + instead of a clobber. ## [2.103.1] - 2026-08-14 ### Fixed diff --git a/plugins/issue-driven-dev/scripts/process-attachments.sh b/plugins/issue-driven-dev/scripts/process-attachments.sh index 70c957d..d0d191e 100755 --- a/plugins/issue-driven-dev/scripts/process-attachments.sh +++ b/plugins/issue-driven-dev/scripts/process-attachments.sh @@ -98,7 +98,18 @@ resolve_repo() { detect_urls() { # Patterns: github.com/user-attachments/{files,assets}/, github.com/{owner}/{repo}/files/N/, - # (private-)user-images.githubusercontent.com/ + # (private-)user-images.githubusercontent.com/, and RELEASE ASSETS (#284): + # github.com/{owner}/{repo}/releases/download/{tag}/{file} + # + # The release-asset form is not an exotic case — it is what THIS PLUGIN itself + # produces. `idd-issue` Step 4 uploads attachments to an `attachments` release + # and writes the release-download URL into the issue body; `idd-diagnose` + # Step 1.5 then called this function, matched nothing, and reported + # "Issue #N has no attachments (empty manifest written)". The write side and + # the read side of the same plugin disagreed about the URL shape, and the + # disagreement was silent: an empty manifest is indistinguishable from an issue + # that genuinely has no attachments, so a diagnosis proceeded without the + # evidence it was supposed to read. # # Fetch and filter are deliberately SPLIT (#186): a zero-attachment issue makes # grep exit 1, and under `set -euo pipefail` a single fetch|filter pipeline dies @@ -117,7 +128,7 @@ detect_urls() { raw=$(gh issue view "$NUMBER" --repo "$REPO" --json body,comments) || return 2 content=$(printf '%s\n' "$raw" | jq -r '.body, .comments[].body') || return 2 printf '%s\n' "$content" \ - | grep -oE 'https://(github\.com/(user-attachments/(files|assets)/[^)]+|[^/]+/[^/]+/files/[0-9]+/[^)]+)|(private-)?user-images\.githubusercontent\.com/[^)]+)' \ + | grep -oE 'https://(github\.com/(user-attachments/(files|assets)/[^)[:space:]]+|[^/]+/[^/]+/files/[0-9]+/[^)[:space:]]+|[^/]+/[^/]+/releases/download/[^/)[:space:]]+/[^)[:space:]]+)|(private-)?user-images\.githubusercontent\.com/[^)[:space:]]+)' \ | sort -u || true } diff --git a/plugins/issue-driven-dev/skills/idd-issue/SKILL.md b/plugins/issue-driven-dev/skills/idd-issue/SKILL.md index c1da07b..e6f829b 100644 --- a/plugins/issue-driven-dev/skills/idd-issue/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-issue/SKILL.md @@ -1020,13 +1020,37 @@ gh release view $ATTACHMENTS_RELEASE --repo $GITHUB_REPO 2>/dev/null || \ gh release create $ATTACHMENTS_RELEASE --repo $GITHUB_REPO \ --title "Attachments" --notes "Issue attachments and figures" +idx=1 # 對 Step 1 蒐集到的每個附件依序上傳(命名規則:issue_${NUMBER}_${DESC}.${ext}) +# +# DESC 必須是 ASCII(#286)。GitHub 會 sanitize release asset 檔名,**非 ASCII 字元 +# 直接被剝除**。實測上傳 `issue_10_tpa2026_年會議程.pdf` 與 +# `issue_10_nstc_心理學門成果發表會議程.pdf`,讀回來是 `issue_10_tpa2026_.pdf` 與 +# `issue_10_nstc_.pdf` —— 中文整段消失。那次沒出事只因為兩者的 ASCII 前綴剛好不同。 +# +# 對中文語境的使用者,DESC 寫成純中文是很自然的,而那會讓兩個不同附件 sanitize 成 +# 同一個名字;再配上 `--clobber`,**第二個會靜默覆蓋第一個**,issue body 兩條連結 +# 指向同一份檔案,而且沒有任何錯誤訊息。 for f in "${ATTACHMENT_PATHS[@]}"; do ext="${f##*.}" - desc=$(make_desc "$f") # 簡短描述 e.g. "snq_timeline" / "telegram_msg_8169455616_photo1" + desc=$(make_desc "$f") # 簡短描述 e.g. "snq_timeline" + # ASCII-fold:非 [A-Za-z0-9._-] 一律轉底線,再摺疊連續底線 + desc=$(printf '%s' "$desc" | LC_ALL=C tr -c 'A-Za-z0-9._-' '_' | sed 's/__*/_/g; s/^_//; s/_$//') + # 全部被摺掉(DESC 原本純非 ASCII)→ 退回可辨識的序號,絕不產生會相撞的空名字 + [ -z "$desc" ] && desc="attachment_$(printf '%03d' "$idx")" upload_name="issue_${NUMBER}_${desc}.${ext}" - gh release upload $ATTACHMENTS_RELEASE "$f" \ - --repo $GITHUB_REPO --clobber + + # --clobber 只有在「確定要覆蓋自己剛上傳的同一份」時才安全。先檢查同名 asset 是否 + # 已存在且不是本輪產生的 —— 存在就換名,不靜默蓋掉別人的附件。 + if gh release view "$ATTACHMENTS_RELEASE" --repo "$GITHUB_REPO" \ + --json assets --jq '.assets[].name' 2>/dev/null | grep -qxF "$upload_name"; then + upload_name="issue_${NUMBER}_${desc}_$(printf '%03d' "$idx").${ext}" + echo "note: asset name collision — uploading as $upload_name instead" >&2 + fi + + gh release upload "$ATTACHMENTS_RELEASE" "$f#$upload_name" \ + --repo "$GITHUB_REPO" + idx=$((idx + 1)) done # 圖片 URL 格式(private 和 public repo 都適用)