diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 38d66f0..1572319 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -15,7 +15,7 @@ "plugins": [ { "name": "issue-driven-dev", - "version": "2.100.0", + "version": "2.101.0", "description": "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/openspec/changes/add-reply-thirdparty-tier-floor/.openspec.yaml b/openspec/changes/add-reply-thirdparty-tier-floor/.openspec.yaml new file mode 100644 index 0000000..65951b6 --- /dev/null +++ b/openspec/changes/add-reply-thirdparty-tier-floor/.openspec.yaml @@ -0,0 +1,4 @@ +schema: spec-driven +created: 2026-07-19 +created_by: che cheng +created_with: claude diff --git a/openspec/changes/add-reply-thirdparty-tier-floor/design.md b/openspec/changes/add-reply-thirdparty-tier-floor/design.md new file mode 100644 index 0000000..a50f8e1 --- /dev/null +++ b/openspec/changes/add-reply-thirdparty-tier-floor/design.md @@ -0,0 +1,32 @@ +## Context + +privacy-scrubbing 契約(v2.87–2.96 已 ship)的 tier 由 repo visibility 決定;gh-egress.sh 的 mechanical net 限 3 個 zero-tolerance items 且 rules 檔明文「不得長成 semantic pattern set、增長需 separate change」。reply 型(v2.100.0)逐字重製第三方原文,marker `` 是 IDD 自產結構化 token。#269 verify DA-3:own-repo 情境永不 ENFORCE,layer-3 只有 prose 自審,必要不充分。in-flight change `add-privacy-scrubbing-gate` 尚有 11 open tasks、標的同兩檔。 + +## Goals / Non-Goals + +**Goals:** + +- layer-3(user-pasted)reply payload 的 tier floor:機械可執行、不論 repo visibility +- net 邊界紀律不破:新 item 僅 token matching(自產 marker),零 semantic +- 與 in-flight change 疊層不衝突(rules 檔 append-only) + +**Non-Goals:** + +- layer 1/2 tier 變更、unattended draft 持久化、semantic 偵測、其他 human-facing 輸出面 + +## Decisions + +**D1 — floor 只綁 layer 3,不綁整個 reply。** layer 1/2 的內容本已在同 repo remote(issue body / comment),重引零新增暴露;全 reply 拉 tier 會把日常 advisor 回覆全部拖進 confirm 流程(比例原則違反、user 明示要評估的軸)。替代案「全 reply enforce」被否。 + +**D2 — 機械 backstop 檢測『自產 marker token』,不是內容。** gh-egress 新 item 4 的判準:SCAN 同時含字串 `type=reply` 與 `points-from=user-pasted`(兩 token 同在 metadata marker 慣例內)且 `$ATTESTED = light` → exit 13(attestation band:這是「attested level 對此 payload 無效」,不是 content leak 的 exit 10)。為何 13 不是 10:net_refuse(10) 語意是 zero-tolerance content leak;本案是 tier-floor violation——attested level 不足,重派時帶 `warn`+完成確認即可,body 本身不必改。marker 可被不寫 marker 繞過 → backstop 定位是 belt-and-suspenders(與既有 net 哲學一致),主 gate 在 SKILL 端手續。 + +**D3 — SKILL 端主 gate:attended 顯式確認、unattended refuse。** layer-3 時 attended → AskUserQuestion「此段第三方逐字內容確認可進 remote?」(帶 redact 選項);unattended(`is_unattended` / directive)→ 不 post、印 refuse 說明+改跑 attended 的指示。理由:reply 本質是 correspondence(人在場的工作),unattended 貼第三方逐字內容無人把關 = CLAUDE.md「raw 第三方逐字內容不進 remote」鐵律的直接風險面。 + +**D4 — rules 檔以 append 為主。** 「Reply layer-3 payload tier floor」段落 append 在 Related rules 之前;既有段落僅允許兩處最小 in-place 校正(net count 句 3→4、growth 歷史句補 3→4),其餘段落(tier 表、ENFORCE 語意、division of labor、implementation contract)零改動——與 in-flight change 的疊層紀律(C_shared_module_coord)以此為界。 + +## Implementation Contract + +- **gh-egress.sh net item 4**:`printf '%s' "$SCAN" | grep -q 'type=reply'` 且 `grep -q 'points-from=user-pasted'` 且 `[ "$ATTESTED" = "light" ]` → stderr 訊息(指示以 `--scrub-attested warn` 重派並先完成使用者確認)+ `exit 13`。attested=warn/enforce 或 marker 不全 → 不觸發、行為不變。位置:既有 3-item net 之後、mention net 之前或之後皆可(獨立判斷)。 +- **SKILL R1 增訂**(R4 僅在 R1 的 floor 條目中被引用、不改動):layer-3 手續字句(attended confirm / unattended refuse)、marker `points-from=user-pasted` 值與 tier floor 的對應、引用 rules 段名。 +- **rules 新段**:normative 三句——LIGHT 不適用於 user-pasted reply payload;最低 WARN+顯式確認;unattended 不 post。net item 4 的邊界聲明(token-only)。 +- **驗證目標**:gh-egress suite 斷言(light+雙 token → exit 13+stderr 含 warn 重派指示;warn+雙 token → 照派;light+單 token(各向)→ 照派;fenced 討論體 → refuse 的 accepted-friction 鎖定;template↔wrapper token binding);idd-comment-reply suite 斷言 SKILL 新字句與 rules 段名;`run-all-tests.sh` 40 suites 全綠;版本 2.100.0 → 2.101.0 三處同步。 diff --git a/openspec/changes/add-reply-thirdparty-tier-floor/proposal.md b/openspec/changes/add-reply-thirdparty-tier-floor/proposal.md new file mode 100644 index 0000000..22b21c0 --- /dev/null +++ b/openspec/changes/add-reply-thirdparty-tier-floor/proposal.md @@ -0,0 +1,37 @@ +## Why + +reply 型(v2.100.0,#269)是唯一逐字重製第三方原文的 comment 型別,但 `SCRUB_LEVEL` 是 repo-visibility-keyed——reply 的典型情境(第三方逐字內容貼到使用者自己的 repo)落在 WARN / LIGHT、永不 ENFORCE;layer-3(使用者貼上的外部原文)是新第三方內容首次進 remote 的唯一通道,目前只靠 prose「heightened 自審」(#269 verify DA-3 判為必要不充分)。 + +## What Changes + +- `rules/privacy-scrubbing.md` 新增 normative 段「Reply layer-3 payload tier floor」:`points-from=user-pasted` 的 reply egress 不適用 LIGHT——最低 WARN+顯式使用者確認(不論 repo visibility);unattended context 下不 post(refuse+說明,留待 attended) +- `skills/idd-comment/SKILL.md` R1/R4:layer-3 source 時 attended → AskUserQuestion 顯式確認第三方逐字內容可進 remote;unattended → refuse post。取代 v2.100.0 的「heightened 自審」prose +- `scripts/gh-egress.sh` 機械 net item 4(3→4,本案即 rules 要求的 separate change):SCAN 含 `type=reply` 且 `points-from=user-pasted` 且 attested=`light` → exit 13 band refuse。僅偵測 IDD 自產 metadata marker token,零 semantic matching——net 邊界紀律不變 +- 測試:gh-egress suite 新斷言(refuse / pass 兩向)+ idd-comment-reply suite 新斷言(SKILL 手續字句) +- **比例原則邊界**:layer 1(comment URL)/ layer 2(issue-body)內容本已在 repo remote、無新增暴露 → 維持 repo-tier 預設,不受本案影響 + +## Non-Goals + +- 不把 reply 全部 payload(layer 1/2)拉 tier——過度(無新增暴露) +- 不做 unattended draft 持久化檔案(YAGNI;refuse+說明即可) +- 不擴 mechanical net 為 semantic 偵測(rules 檔明文禁止;item 4 限自產 marker token) +- 不動 in-flight `add-privacy-scrubbing-gate` 的未完成 tasks(rules 檔僅 append 新段) +- 不涵蓋 PR body / Discussions 等其他 human-facing 輸出面的同類問題(horizon 另案) + +## Capabilities + +### New Capabilities + +(none) + +### Modified Capabilities + +- `idd-comment-reply`: MODIFIED Points-source resolution(layer-3 綁 tier floor)+ ADDED requirement「Layer-3 third-party payload tier floor」(rules 段、SKILL 手續、gh-egress 機械 backstop 三件套的 normative 契約) + +## Impact + +- Affected specs: `idd-comment-reply`(delta:1 MODIFIED + 1 ADDED requirement) +- Affected code: + - New: (none) + - Modified: plugins/issue-driven-dev/rules/privacy-scrubbing.md, plugins/issue-driven-dev/scripts/gh-egress.sh, plugins/issue-driven-dev/skills/idd-comment/SKILL.md, plugins/issue-driven-dev/scripts/tests/gh-egress/test.sh, plugins/issue-driven-dev/scripts/tests/idd-comment-reply/test.sh, plugins/issue-driven-dev/CHANGELOG.md, plugins/issue-driven-dev/.claude-plugin/plugin.json, .claude-plugin/marketplace.json + - Removed: (none) diff --git a/openspec/changes/add-reply-thirdparty-tier-floor/specs/idd-comment-reply/spec.md b/openspec/changes/add-reply-thirdparty-tier-floor/specs/idd-comment-reply/spec.md new file mode 100644 index 0000000..3d94966 --- /dev/null +++ b/openspec/changes/add-reply-thirdparty-tier-floor/specs/idd-comment-reply/spec.md @@ -0,0 +1,56 @@ +## MODIFIED Requirements + +### Requirement: Points-source resolution + +`--points-from` is required (per the Reply comment type requirement); its *value* SHALL resolve through a three-layer chain: (1) an explicit comment URL → points are taken from that comment's blockquote / enumerated list; (2) the literal `issue-body` — or an explicit URL that yields no enumerable point list — → the points SHALL be taken from the verbatim "Original text" blockquote(s) of the issue body; (3) when neither yields points, the skill SHALL ask the user to paste the original text. The three layers describe how the required value resolves — they are NOT a default for an absent flag (an absent flag is refused at Step 2). Resolved points SHALL be reproduced verbatim in the reply; paraphrasing the counterpart's original wording SHALL NOT occur. Verbatim reproduction is nonetheless subject to the privacy-scrub gate: when a quoted point carries private / PII content, the scrub gate takes precedence over verbatim (redaction wins on conflict). Layer-3 (user-pasted) content is additionally subject to the third-party payload tier floor (see the "Layer-3 third-party payload tier floor" requirement): it SHALL NOT dispatch under the LIGHT tier, and the metadata marker SHALL record `points-from=user-pasted` so the floor is mechanically detectable. + +#### Scenario: Value issue-body resolves to the issue-body blockquote + +- **WHEN** `--points-from=issue-body` is given and the issue body contains an Original text blockquote +- **THEN** the points are extracted from that blockquote and quoted verbatim in the reply draft + +#### Scenario: No resolvable source falls back to user input + +- **WHEN** neither an explicit source nor an issue-body blockquote yields points +- **THEN** the skill asks the user to provide the original text and does not fabricate or summarize points on its own + +#### Scenario: Layer-3 resolution marks the marker + +- **WHEN** points are resolved from user-pasted text (layer 3) +- **THEN** the rendered metadata marker records `points-from=user-pasted` + +## ADDED Requirements + +### Requirement: Layer-3 third-party payload tier floor + +Reply drafts whose points source is user-pasted external content (layer 3) SHALL NOT dispatch under the LIGHT scrub tier regardless of repository visibility: the minimum is WARN accompanied by an explicit user confirmation that the quoted third-party verbatim content may be pushed to the remote. The floor is a minimum, not a replacement — the effective tier SHALL be the stricter of the repository-derived tier and WARN, so an ENFORCE tier (third-party repository) is never downgraded by the confirmation. In an unattended context the skill SHALL refuse to post such a reply (with an explanatory notice deferring to an attended session) rather than dispatch it unconfirmed. Layer-1 and layer-2 sources SHALL remain governed by the repository-visibility tier default only when the quoted content is already present on the destination repository's remote; a layer-1 comment URL belonging to a different repository SHALL be treated as an external source carrying this same floor (the marker records `points-from=user-pasted`). As a deterministic backstop, `gh-egress.sh` SHALL refuse dispatch (attestation exit-code band) when the drafted body contains both the `type=reply` and `points-from=user-pasted` marker tokens while the attested level is `light`; this check matches IDD's own structured metadata marker tokens only and SHALL NOT grow into semantic content matching. + +#### Scenario: Light-tier dispatch of user-pasted reply is refused mechanically + +- **WHEN** a body containing both `type=reply` and `points-from=user-pasted` marker tokens is dispatched with `--scrub-attested light` +- **THEN** `gh-egress.sh` refuses in the attestation exit band and instructs re-dispatch at `warn` after explicit user confirmation + +#### Scenario: Warn-tier dispatch after confirmation proceeds + +- **WHEN** the repository-derived tier is LIGHT or WARN and the same body is dispatched with `--scrub-attested warn` after the user confirmed the quoted content +- **THEN** the wrapper dispatches normally + +#### Scenario: Enforce tier is never downgraded by confirmation + +- **WHEN** the destination is a third-party repository (repository-derived tier ENFORCE) and a layer-3 reply is drafted +- **THEN** the ENFORCE block-with-diff flow runs unchanged and dispatch is not attested below `enforce` + +#### Scenario: Cross-repo layer-1 source carries the floor + +- **WHEN** `--points-from` is an explicit comment URL belonging to a repository other than the destination +- **THEN** the source is treated as layer 3 (marker records `points-from=user-pasted`) and the floor applies + +#### Scenario: Layer-1/2 replies are unaffected + +- **WHEN** a reply whose marker records `points-from=issue-body` is dispatched with `--scrub-attested light` on a private repo +- **THEN** the wrapper dispatches normally (the floor binds layer 3 only) + +#### Scenario: Unattended layer-3 reply is not posted + +- **WHEN** the reply pipeline runs in an unattended context and the points source resolved to user-pasted text +- **THEN** the skill refuses to post, explains the tier-floor reason, and defers to an attended session diff --git a/openspec/changes/add-reply-thirdparty-tier-floor/tasks.md b/openspec/changes/add-reply-thirdparty-tier-floor/tasks.md new file mode 100644 index 0000000..662ff0a --- /dev/null +++ b/openspec/changes/add-reply-thirdparty-tier-floor/tasks.md @@ -0,0 +1,16 @@ +## 1. Normative 契約落地 + +- [x] 1.1 plugins/issue-driven-dev/rules/privacy-scrubbing.md append 新段「Reply layer-3 payload tier floor」:LIGHT 不適用於 `points-from=user-pasted` reply payload、最低 WARN+顯式使用者確認(不論 repo visibility)、unattended 不 post;net item 4 的 token-only 邊界聲明(spec requirement: Layer-3 third-party payload tier floor)。append 為主(design D4):新段可 grep;既有段僅 net-count 句與 growth 歷史句兩處最小校正,其餘零改動。 +- [x] 1.2 plugins/issue-driven-dev/scripts/gh-egress.sh 加 net item 4:SCAN 同時含 `type=reply` 與 `points-from=user-pasted` 且 `$ATTESTED = light` → stderr 指示(以 `--scrub-attested warn` 重派、先完成使用者確認)+ exit 13(design D2:attestation band 非 content band)。完成判準:task 2.1 兩向測試綠。 +- [x] 1.3 plugins/issue-driven-dev/skills/idd-comment/SKILL.md R1/R4 增訂 layer-3 手續:attended → AskUserQuestion 顯式確認第三方逐字內容可進 remote(帶 redact 選項);unattended → refuse post+說明;marker 記 `points-from=user-pasted`;引用 rules 新段名;移除/改寫 v2.100.0 的「heightened 自審」句為指向新 normative 段(spec requirement: Layer-3 third-party payload tier floor + MODIFIED Points-source resolution)。完成判準:新字句可 grep、舊 prose 不殘留矛盾表述。 + +## 2. 測試 + +- [x] 2.1 plugins/issue-driven-dev/scripts/tests/gh-egress/test.sh 加三向斷言:(a) light+雙 marker → exit 13;(b) warn+雙 marker → 照派(mock gh 或既有測試手法);(c) light+僅 `type=reply`(無 user-pasted)→ 照派。跟隨該 suite 既有 mock/fixture 慣例。完成判準:單跑 suite 綠;暫時移除 item 4 時 (a) 紅(RED 驗證後還原)。 +- [x] 2.2 plugins/issue-driven-dev/scripts/tests/idd-comment-reply/test.sh 加斷言:SKILL 的 layer-3 attended-confirm 與 unattended-refuse 字句、rules 新段名引用、marker `points-from=user-pasted` tier-floor 對應。完成判準:suite 綠(27 → 31±)。 +- [x] 2.3 bash plugins/issue-driven-dev/scripts/run-all-tests.sh 全綠(40 suites 0 fail)。完成判準:aggregator 輸出 0 fail。 + +## 3. 文件與版本 + +- [x] 3.1 plugins/issue-driven-dev/CHANGELOG.md 加 2.101.0 段(tier floor 三件套、net 3→4 的 separate-change 依據、#272 / #269 DA-3 出處);plugin.json 與 root marketplace.json entry 同步 2.101.0。完成判準:三處版本字串一致。 +- [ ] 3.2 spectra validate + analyze 無 Critical/Warning;PR 以 Refs #272 開出。完成判準:validate exit 0。 diff --git a/plugins/issue-driven-dev/.claude-plugin/plugin.json b/plugins/issue-driven-dev/.claude-plugin/plugin.json index cc5da55..df3f8f6 100644 --- a/plugins/issue-driven-dev/.claude-plugin/plugin.json +++ b/plugins/issue-driven-dev/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "issue-driven-dev", "description": "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.", - "version": "2.100.0", + "version": "2.101.0", "author": { "name": "Che Cheng" }, diff --git a/plugins/issue-driven-dev/CHANGELOG.md b/plugins/issue-driven-dev/CHANGELOG.md index 11cbc8e..3705974 100644 --- a/plugins/issue-driven-dev/CHANGELOG.md +++ b/plugins/issue-driven-dev/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.101.0] - 2026-07-19 + +### Added + +- **Reply layer-3 third-party payload tier floor (#272, Spectra `add-reply-thirdparty-tier-floor`)** — closes the #269 verify DA-3 gap: `--type=reply` is the only type mandating third-party verbatim reproduction, yet `SCRUB_LEVEL` is repo-visibility-keyed, so the characteristic case (third-party words posted to the user's OWN repo) resolves to WARN/LIGHT and never ENFORCE. Proportionality analysis: layers 1–2 (comment URL / issue-body) re-quote content already on this repo's remote — zero new exposure, tier default kept; **layer 3 (user-pasted external text) is the one channel where NEW third-party verbatim content first reaches the remote** — so the floor binds layer 3 only. Three-piece contract: (1) `rules/privacy-scrubbing.md` new normative section — LIGHT does not apply to `points-from=user-pasted` replies regardless of visibility; minimum WARN + explicit user confirmation; unattended contexts refuse to post (no human present to confirm); (2) SKILL R1 — attended AskUserQuestion confirm (with redact option) / unattended refuse, replacing v2.100.0's necessary-but-insufficient "heightened self-review" prose; (3) `gh-egress.sh` mechanical net item 4 (3→4, the separate change the rules file requires): body carrying both `type=reply` and `points-from=user-pasted` marker tokens at attested `light` → exit 13 (attestation band — the level is invalid for this payload, the body needs no redaction). Matches IDD's OWN structured metadata marker tokens only — zero semantic content matching, net boundary discipline unchanged. Marker-less bodies bypass by construction: the SKILL confirm is the primary gate, the wrapper is belt-and-suspenders. + +### Tests + +- gh-egress suite +3 (tier-floor refuse at light / dispatch at warn / layer-1-2 unaffected — 60 assertions); idd-comment-reply suite +6 (SKILL floor clauses + rules section anchors — 33 assertions). Aggregator 40 suites, 0 fail. + ## [2.100.0] - 2026-07-18 ### Added diff --git a/plugins/issue-driven-dev/rules/privacy-scrubbing.md b/plugins/issue-driven-dev/rules/privacy-scrubbing.md index ca682b1..4df6d9e 100644 --- a/plugins/issue-driven-dev/rules/privacy-scrubbing.md +++ b/plugins/issue-driven-dev/rules/privacy-scrubbing.md @@ -114,7 +114,7 @@ things and **only** these: *existence* of the gate deterministic even though its *content* is the LLM's. (Q1 resolution: mechanism (a), a required per-call flag — not an env var, which could be left globally set and silently satisfy every dispatch.) -2. **A mechanical last-resort net** catching **only 3 zero-tolerance mechanical +2. **A mechanical last-resort net** catching **only 4 mechanical items** — an absolute `/Users/` home path, verbatim `~/.claude.json` content, and an unattested raw `@login` mention token (#117) — as belt-and-suspenders if the LLM misses one. It is **level-independent** @@ -125,9 +125,11 @@ things and **only** these: wrapper — that breadth is the LLM self-review's job. The wrapper MUST NOT grow a semantic check; the #202 D1/D2 boundary is -"mechanical token matching only". The 2→3 growth (#117 mention net) stayed on -the mechanical side of that line; any future semantic expansion requires a -separate openspec change (spec: "net does not grow into semantic matching"). +"mechanical token matching only". The 2→3 growth (#117 mention net) and the +3→4 growth (#272 reply tier-floor backstop — matching IDD's own metadata +marker tokens, not content) both stayed on the mechanical side of that line; +any future semantic expansion requires a separate openspec change (spec: "net +does not grow into semantic matching"). ## Division of labor vs `sanitize_source_label` (#75) @@ -169,6 +171,56 @@ Every IDD egress skill (`idd-issue`, `idd-comment`, `idd-edit`, `idd-diagnose`, Phase 2 follow-up call-site substitutions (the logic already lives in the wrapper). +## Reply layer-3 payload tier floor (#272) + +`idd-comment --type=reply` is the only comment type that mandates verbatim +reproduction of a third party's words. Its three points-source layers carry +unequal risk: layers 1–2 (an existing comment / the issue body) re-quote +content **already on this repository's remote** — zero new exposure — while +layer 3 (**user-pasted external text**: an email, a DM, meeting notes) is the +one channel where NEW third-party verbatim content first reaches the remote. +Because the strictness level is repo-visibility-keyed, the characteristic reply +case (third-party words posted to the user's OWN repo) resolves to WARN/LIGHT +and never ENFORCE — so for layer-3 payloads the tier default is not enough: + +1. **LIGHT does not apply** to a reply whose points source is user-pasted + (marker `points-from=user-pasted`), regardless of repository visibility. + The minimum is **WARN plus an explicit user confirmation** that the quoted + third-party content may be pushed to the remote. +2. **Unattended contexts SHALL NOT post** such a reply: refuse with an + explanatory notice and defer to an attended session (no human is present to + give the confirmation the floor requires). +3. Layers 1–2 remain governed by the repository-visibility default (the floor + binds layer 3 only — proportionality). + +The floor is a **minimum, not a replacement**: the effective tier is +`max(repo-derived tier, WARN)` — a third-party repo's ENFORCE stays ENFORCE +(block-with-diff runs unchanged); confirmation never downgrades a stricter +tier. The layer-1/2 exemption presupposes the comment URL belongs to the +**destination repository**; a cross-repo comment URL is an external source and +carries the layer-3 floor (re-quoting another repo's content here is a new +cross-audience disclosure). + +Deterministic backstop: `gh-egress.sh` net item 4 refuses (attestation band, +exit 13) when the drafted body carries both the `type=reply` and +`points-from=user-pasted` **token substrings** while the attested level is +`light`. Honest mechanics: the check is two raw fixed-string matches over the +whole drafted prose (fences NOT stripped — fenced content still reaches the +remote, and stripping would be a trivial bypass), so a body merely *discussing* +both tokens at `light` over-refuses; that friction is accepted (fail-safe, +escapable by re-attesting at `warn`), and — unlike the entity-encoded `@` +net — there is **no backtick escape hatch** for it. Item 4 is +**level-dependent** (fires only at `light`) — a tier-floor attestation check, +unlike items 1–3 which are level-independent zero-tolerance nets. It matches +token substrings from IDD's own marker vocabulary — it is not, and must not +grow into, semantic content matching. A marker-less body bypasses the backstop +by construction, and the unattended-refuse clause above is SKILL-prose-enforced +(the wrapper has no unattended signal — a deliberate boundary): the SKILL-side +confirmation step is the primary gate; the wrapper is belt-and-suspenders. +Anchor: the CLAUDE.md "raw third-party verbatim content does not go to remote" +iron rule — this floor is its mechanical enforcement for the one channel reply +opened. + ## Related rules - [`tagging-collaborators.md`](tagging-collaborators.md) — the sibling diff --git a/plugins/issue-driven-dev/scripts/gh-egress.sh b/plugins/issue-driven-dev/scripts/gh-egress.sh index 73b62db..7f88f41 100755 --- a/plugins/issue-driven-dev/scripts/gh-egress.sh +++ b/plugins/issue-driven-dev/scripts/gh-egress.sh @@ -21,20 +21,25 @@ # guarantees the step EXISTS; it does not pretend to guarantee the # judgment was correct (that is the LLM's + the ENFORCE block-with-diff's # job — see rules/privacy-scrubbing.md). -# (b) A tiny mechanical LAST-RESORT net catching ONLY 3 zero-tolerance MECHANICAL -# items, as belt-and-suspenders for an LLM miss: +# (b) A tiny mechanical LAST-RESORT net catching ONLY 4 MECHANICAL items, as +# belt-and-suspenders for an LLM miss: # 1. an absolute macOS home path `/Users/` # 2. verbatim `~/.claude.json` content (a project-path string copied out # of the user's actual ~/.claude.json `projects` object). The bare # filename token is PUBLIC (Anthropic docs) and deliberately NOT # matched — content is the secret, not the name (#203 item 1). -# This net is LEVEL-INDEPENDENT (fires even at LIGHT) because these two are -# absolute zero-tolerance leaks, not "ordinary identifiers". +# 3. an unattested raw @login mention token (#117) +# 4. reply layer-3 tier floor (#272): `type=reply` + `points-from=user-pasted` +# token substrings at attested `light` +# Items 1-3 are LEVEL-INDEPENDENT zero-tolerance nets (fire even at LIGHT: +# absolute leaks / irreversible notifications). Item 4 is LEVEL-DEPENDENT +# (fires only at `light`) — a tier-floor attestation check, not a content +# leak: the same body dispatches at warn/enforce. # # WHAT IT MUST NOT DO # No semantic pattern matching. No maintained denylist. No name detection. The # semantic breadth of "is this private?" is 100% the LLM self-review's job -# (design D1). Expanding this net beyond the 2 literal items requires a +# (design D1). Expanding this net beyond these literal items requires a # separate openspec change (spec: "net does not grow into semantic matching"). # # ATTESTATION FORMAT — Open Question Q1 resolution (chosen at apply time) @@ -181,8 +186,9 @@ case "$ATTESTED" in exit 13 ;; esac -# --- (b) mechanical last-resort net (3 zero-tolerance mechanical items) ------- -# (grown 2→3 by #117 mention net — mechanical token matching, NOT semantic; +# --- (b) mechanical last-resort net (4 zero-tolerance mechanical items) ------- +# (grown 2→3 by #117 mention net, 3→4 by #272 reply tier-floor backstop — +# mechanical token matching, NOT semantic; # the "no semantic matching" boundary from #202 D1/D2 is unchanged) # Joined once; the net only ever inspects the drafted prose, never --repo / # --label / --milestone etc. (so metadata-only edits are never false-flagged). @@ -329,6 +335,33 @@ if [ -n "$UNATTESTED_MENTIONS" ]; then exit 11 fi +# 4. reply layer-3 tier floor (#272). A reply whose points source is user-pasted +# external text is the ONLY channel where NEW third-party verbatim content +# first reaches the remote (layers 1/2 re-quote content already on this +# repo's remote). rules/privacy-scrubbing.md § "Reply layer-3 payload tier +# floor": LIGHT does not apply to that payload — minimum WARN plus an +# explicit user confirmation, regardless of repo visibility. +# Mechanism: token match on IDD's OWN structured metadata marker +# (`type=reply` + `points-from=user-pasted`, both emitted by the reply +# template) — deterministic, ZERO semantic content matching (the net +# boundary from #202 D1/D2 is unchanged; growing the net 3→4 is exactly the +# "separate change" the rules file requires, this one). +# Exit 13 (attestation band), NOT 10: the ATTESTED LEVEL is invalid for this +# payload — re-dispatch at max(repo tier, warn) after the confirmation +# (whether the body itself needs redaction is the higher tier's self-review +# judgment, not this check's). Marker-less bodies bypass this backstop by construction — +# the SKILL-side confirm step is the primary gate; this is belt-and-suspenders. +if [ "$ATTESTED" = "light" ] \ + && printf '%s' "$SCAN" | grep -Fq -- 'type=reply' \ + && printf '%s' "$SCAN" | grep -Fq -- 'points-from=user-pasted'; then + echo "✗ gh-egress: REFUSED — reply with user-pasted third-party payload attested at 'light' (#272 tier floor)." >&2 + echo " Layer-3 (user-pasted) reply content is new third-party verbatim material entering the remote;" >&2 + echo " LIGHT does not apply regardless of repo visibility (rules/privacy-scrubbing.md § Reply layer-3 payload tier floor)." >&2 + echo " Obtain the user's explicit confirmation that the quoted content may be posted, then re-dispatch" >&2 + echo " with --scrub-attested warn (or enforce)." >&2 + exit 13 +fi + # --- dispatch: byte-for-byte identical to raw `gh issue ...` ----------- GH_BIN="${IDD_GH_BIN:-gh}" # ${arr[@]+...} idiom: empty array expands to NOTHING (":-" would yield one diff --git a/plugins/issue-driven-dev/scripts/tests/gh-egress/test.sh b/plugins/issue-driven-dev/scripts/tests/gh-egress/test.sh index 4c4ebe3..b9722ae 100644 --- a/plugins/issue-driven-dev/scripts/tests/gh-egress/test.sh +++ b/plugins/issue-driven-dev/scripts/tests/gh-egress/test.sh @@ -333,4 +333,52 @@ bash "$SCRIPT" comment 5 --repo o/r \ --body "we store vault material under /srv/other-vault/keyring generally" "${ATT[@]}" >/dev/null 2>&1 assert_exit "similar-but-absent path NOT caught (#225 no fuzzy matching) → dispatch (exit 0)" 0 $? +# ── #272 reply layer-3 tier floor — marker-token backstop (net item 4) ──────── +# Body carrying BOTH IDD-own marker tokens (`type=reply` + `points-from=user-pasted`) +# at attested level `light` → refuse in the ATTESTATION band (13, not content 10): +# the attested level is invalid for this payload; re-dispatch at warn after the +# explicit user confirmation. Token matching on IDD's own metadata marker only — +# NOT semantic content matching (net boundary discipline preserved). +REPLY_L3_BODY='## 🧑‍🏫 Reply +> 「third-party pasted words」 +done. ' +bash "$SCRIPT" comment 5 --repo o/r --body "$REPLY_L3_BODY" --scrub-attested light >/dev/null 2>&1 +assert_exit "reply user-pasted marker at light → tier-floor refuse (#272, exit 13)" 13 $? +# same body at warn (user confirmed) → dispatches normally +bash "$SCRIPT" comment 5 --repo o/r --body "$REPLY_L3_BODY" --scrub-attested warn >/dev/null 2>&1 +assert_exit "reply user-pasted marker at warn → dispatch (#272 floor satisfied, exit 0)" 0 $? +# reply WITHOUT user-pasted (layer 1/2) at light → unaffected (floor binds layer 3 only) +REPLY_L2_BODY='## 🧑‍🏫 Reply +> 「quoted from issue body」 +done. ' +bash "$SCRIPT" comment 5 --repo o/r --body "$REPLY_L2_BODY" --scrub-attested light >/dev/null 2>&1 +assert_exit "reply issue-body marker at light → dispatch (#272 layer-1/2 unaffected, exit 0)" 0 $? +# conjunction pin: user-pasted token ALONE (no type=reply) at light → dispatch +# (kills the single-token mis-implementation that would pass the other fixtures) +bash "$SCRIPT" comment 5 --repo o/r --body "note: source resolution recorded points-from=user-pasted here" --scrub-attested light >/dev/null 2>&1 +assert_exit "user-pasted token without type=reply at light → dispatch (#272 conjunction pinned, exit 0)" 0 $? +# refusal stderr carries the remediation instruction (re-dispatch at warn) +ERR="$(bash "$SCRIPT" comment 5 --repo o/r --body "$REPLY_L3_BODY" --scrub-attested light 2>&1 >/dev/null)" +printf '%s' "$ERR" | grep -Fq -- '--scrub-attested warn' +assert_exit "tier-floor stderr instructs re-dispatch at warn (#272)" 0 $? +# accepted-friction LOCK: a body merely DISCUSSING both tokens (in a code fence) +# at light IS refused — raw-substring semantics are deliberate (fenced content +# still reaches the remote; stripping fences would be a trivial bypass). +DISCUSS_BODY='documenting the marker vocabulary: +``` +type=reply +points-from=user-pasted +``` +this body quotes both tokens without being a reply.' +bash "$SCRIPT" comment 5 --repo o/r --body "$DISCUSS_BODY" --scrub-attested light >/dev/null 2>&1 +assert_exit "fenced discussion of both tokens at light → refuse (#272 accepted friction locked, exit 13)" 13 $? + +# template↔wrapper binding (#272 DA N-4): the SKILL template must emit the exact +# vocabulary the wrapper greps — if either side's spelling drifts, the net is +# silently disabled. Bind both ends to the shared literals. +SKILL_MD="$HERE/../../../skills/idd-comment/SKILL.md" +assert_output_grep "binding: SKILL template enumerates user-pasted" "points-from={comment-url|issue-body|user-pasted}" "$SKILL_MD" +assert_output_grep "binding: wrapper greps the same reply token" "grep -Fq -- 'type=reply'" "$SCRIPT" +assert_output_grep "binding: wrapper greps the same user-pasted token" "grep -Fq -- 'points-from=user-pasted'" "$SCRIPT" + print_summary "gh-egress" diff --git a/plugins/issue-driven-dev/scripts/tests/idd-comment-reply/test.sh b/plugins/issue-driven-dev/scripts/tests/idd-comment-reply/test.sh index 5a3f353..18122a0 100644 --- a/plugins/issue-driven-dev/scripts/tests/idd-comment-reply/test.sh +++ b/plugins/issue-driven-dev/scripts/tests/idd-comment-reply/test.sh @@ -44,6 +44,17 @@ assert_output_grep "skill: layer-3 user-paste fallback" "要求使用者 assert_output_grep "skill: verbatim ban on counterpart's words" "禁止 paraphrase 對方原文" "$SKILL" assert_output_grep "skill: scrub wins over verbatim on PII" "scrub 優先於 verbatim" "$SKILL" +# ── layer-3 tier floor (#272; spec: Layer-3 third-party payload tier floor) ── +RULES="$PLUGIN_ROOT/rules/privacy-scrubbing.md" +assert_output_grep "skill: LIGHT does not apply to user-pasted" "LIGHT 不適用於 \`points-from=user-pasted\`" "$SKILL" +assert_output_grep "skill: floor = WARN + explicit confirm" "WARN + AskUserQuestion 顯式確認" "$SKILL" +assert_output_grep "skill: unattended layer-3 not posted" "Unattended context 下不 post" "$SKILL" +assert_output_grep "skill: cites the rules floor section" "Reply layer-3 payload tier floor" "$SKILL" +assert_output_grep "rules: floor section exists" "## Reply layer-3 payload tier floor" "$RULES" +assert_output_grep "rules: net item 4 is token-only" "token substrings from IDD's own marker vocabulary" "$RULES" +assert_output_grep "rules: floor is minimum not replacement" "max(repo-derived tier, WARN)" "$RULES" +assert_output_grep "rules: cross-repo layer-1 carries the floor" "cross-repo comment URL is an external source" "$RULES" + # ── verify-before-claim gate (spec: Verify-before-claim gate) ── assert_output_grep "skill: evidence check before claiming" "git log --grep" "$SKILL" assert_output_grep "skill: evidence is per-point not per-issue (DA-1)" "證據是 per-point 的" "$SKILL" diff --git a/plugins/issue-driven-dev/skills/idd-comment/SKILL.md b/plugins/issue-driven-dev/skills/idd-comment/SKILL.md index a6afc18..407aec0 100644 --- a/plugins/issue-driven-dev/skills/idd-comment/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-comment/SKILL.md @@ -348,7 +348,12 @@ fi reply 是**寫給人看的 correspondence**,不是 audit log。draft 依以下順序執行,順序本身是契約: -**R1 — Points-source 解析(`--points-from`,三層鏈)**:flag 必填(Step 2 validation 擋缺席)。值域:comment URL(逐點取自該 comment 的 blockquote / 列點)或字面值 `issue-body`。`issue-body`(或 URL 解析不到列點)→ 預設抓 issue body 的 Original text blockquote(idd-issue 建案紀律寫入的逐字原文)→ 仍解析不到 → 要求使用者貼上原文,**不得**自行歸納。逐點內容一律 verbatim blockquote,**禁止 paraphrase 對方原文**——收件人看到自己的話被改寫即失去信任(IC_R007 同源紀律)。**但 verbatim 服從 privacy-scrub gate**:reply 是唯一逐字重製第三方原文的型別,當某點引文含私人/PII 內容(尤其 layer 3 使用者貼上的外部原文),**scrub 優先於 verbatim(衝突時 redaction 勝)**——egress 的 `--scrub-attested` 本就掃整個 body 含 blockquote,此處明文化該優先序,避免執行者過度字面化 verbatim 而把未遮蔽的第三方 PII 推上 remote。**注意 SCRUB_LEVEL 是 repo-visibility-keyed**(third-party=enforce / own-public=warn / own-private=light):reply 的典型情境是「第三方逐字內容貼到使用者自己的 repo」→ 落在 warn / light(預設 proceed),**不會**觸發 enforce 的 block-with-diff。因此對 **layer 3 使用者貼上的外部原文**(機械網抓不到的人名/未發表結果等 human-prose PII),executor **必須主動施加 heightened 隱私自審**、不得僅倚賴 repo-tier 預設放行;比照 CLAUDE.md「raw 第三方逐字內容不進 remote」鐵律。(把 reply 第三方 payload 強制拉到 enforce tier 屬 privacy-scrubbing 跨切面決策,另案 follow-up。) +**R1 — Points-source 解析(`--points-from`,三層鏈)**:flag 必填(Step 2 validation 擋缺席)。值域:comment URL(逐點取自該 comment 的 blockquote / 列點)或字面值 `issue-body`。`issue-body`(或 URL 解析不到列點)→ 預設抓 issue body 的 Original text blockquote(idd-issue 建案紀律寫入的逐字原文)→ 仍解析不到 → 要求使用者貼上原文,**不得**自行歸納。逐點內容一律 verbatim blockquote,**禁止 paraphrase 對方原文**——收件人看到自己的話被改寫即失去信任(IC_R007 同源紀律)。**但 verbatim 服從 privacy-scrub gate**:reply 是唯一逐字重製第三方原文的型別,當某點引文含私人/PII 內容(尤其 layer 3 使用者貼上的外部原文),**scrub 優先於 verbatim(衝突時 redaction 勝)**——egress 的 `--scrub-attested` 本就掃整個 body 含 blockquote,此處明文化該優先序,避免執行者過度字面化 verbatim 而把未遮蔽的第三方 PII 推上 remote。**Layer-3 tier floor(#272,normative — 見 rules/privacy-scrubbing.md § Reply layer-3 payload tier floor)**:SCRUB_LEVEL 是 repo-visibility-keyed(third-party=enforce / own-public=warn / own-private=light),reply 的典型情境(第三方逐字內容貼到使用者自己的 repo)落在 warn / light、永不 enforce——但 layer 1/2 的內容本已在本 repo remote(零新增暴露),**唯一的新增暴露通道是 layer 3(使用者貼上的外部原文)**。故 tier floor 只綁 layer 3: + +- **LIGHT 不適用於 `points-from=user-pasted` 的 reply**(不論 repo visibility)。最低 **WARN + AskUserQuestion 顯式確認**「此段第三方逐字內容確認可進 remote?」(附 redact 選項)。**Floor 是下限不是替代**:確認通過後以 `max(repo-derived tier, warn)` 派送——repo tier 為 light → 升 `warn`;為 warn → 維持 `warn`;為 **enforce(third-party repo)→ 維持 enforce**,block-with-diff 流程照常跑,**不得**因確認而降級。 +- **Unattended context 下不 post**:layer-3 reply 在 unattended(`is_unattended` / UNATTENDED MODE directive)一律 refuse + 印說明(tier floor 需要人的確認),留待 attended session。 +- Marker 必記 `points-from=user-pasted`,讓 gh-egress 的機械 backstop(net item 4:雙 marker token + attested=light → exit 13 refuse)可 deterministic 兜底。marker 是 belt-and-suspenders,本手續才是主 gate。 +- Layer 1/2(comment URL / issue-body)維持 repo-tier 預設,不受 floor 影響(比例原則)——**前提是 layer-1 的 comment URL 屬於 destination repo**(「內容已在同 repo remote、零新增暴露」的豁免理由才成立)。**跨 repo 的 comment URL 是 external source**:解析時視同 layer 3(marker 記 `points-from=user-pasted`、套用本 floor)——把別的(尤其 private / 第三方)repo 的內容逐字帶進本 repo 是一次新的跨受眾揭露。 **R2 — verify-before-claim gate(per-point,非 issue-level)**:每一點在 draft 宣稱「已解決」之前,先驗證證據存在。**證據是 per-point 的,不是 per-issue**:`git log --grep "#N"` 只是**入口**——找到的 commit / merged PR **必須實際包含處理『該點』的改動**(讀 diff 確認觸及該點所指的 file / function / theorem / behavior),才算該點的證據。**嚴禁**「找到任一提及 `#N` 的 commit 就把所有點都標已解決」——那正是本 gate 要防的 over-claim(本 type 的 raison d'être:每句『已修正』都指到真實 diff)。某點找不到對應該點的具體改動 → 該點寫 open / pending,不得宣稱完成。與 idd-close Step 1.6 semantic gate 同族;prose 紀律,不另立 helper script。 @@ -405,6 +410,9 @@ echo "$COMMENT_BODY" > /tmp/idd-comment-$$.md # (#226)egress 經 gh-egress.sh 派送:$SCRUB_LEVEL 依 rules/privacy-scrubbing.md 解析 # (third-party=enforce / own-public=warn / private=light),派送前先跑 LLM 隱私自審; # 有 @mention 時另帶 --mention-attested(rules/tagging-collaborators.md 5-step 後)。 +# (#272)reply 且 points-from=user-pasted 時,SCRUB_LEVEL 先取 floor: +# [ "$SCRUB_LEVEL" = light ] && SCRUB_LEVEL=warn # max(repo tier, warn);enforce 維持 enforce +# —— SKILL 端是主 gate(wrapper net item 4 只是 light 的機械兜底)。 bash "$CLAUDE_PLUGIN_ROOT/scripts/gh-egress.sh" comment $NUMBER --repo $GITHUB_REPO --body-file /tmp/idd-comment-$$.md \ --scrub-attested "$SCRUB_LEVEL" ${MENTION_ATTESTED:+--mention-attested="$MENTION_ATTESTED"} rm /tmp/idd-comment-$$.md