Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions plugins/issue-driven-dev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,18 @@ attachments_release: "attachments"
---
```

### 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.

**Default-ON**, opt-out via config. Tagging includes a `git push` (a repo-wide side effect), is idempotent (existing tag → skip), and graceful-skips on any push failure (never aborts the workflow). Turn it off with:

```json
{ "auto_tag": { "enabled": false } }
```

Full schema (`enabled` / `baseline_format` / `verified_format` / `push_remote`) in [references/config-protocol.md](references/config-protocol.md) → `auto_tag` field.

## Deep Integrations(深度整合套件總覽)

IDD 遵循「**深度整合 >> hard-coded**」原則(#209/#214):生態系已有 canonical 套件時依賴它、不在內部複製等價邏輯。每個整合的綁定形狀與缺席行為:
Expand Down
24 changes: 24 additions & 0 deletions plugins/issue-driven-dev/references/config-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,30 @@ An OPTIONAL identity registry so IDD can resolve a person's alias / email / disp

**PII boundary (important).** `email` is personally-identifiable and MUST NOT live in a committed / public config. Keep the non-PII fields (`github_login` / `display_name` / `role` / `aliases`) in the normal walked-up config; put `email` only in a **private / gitignored** config layer. `idd-config validate` emits a PII reminder whenever it sees an `email` in a registry entry, so a leak into a tracked config is surfaced early. This mirrors the git-privacy boundary: a person's raw email is third-party PII, not your own derivative content.

### `auto_tag` field

An OPTIONAL object that turns on **selective git-tag automation** at two high-value IDD milestones (#85): `idd-issue` tags the rollback baseline, `idd-verify` tags the review-ready snapshot. Deliberately *selective* — only these two points get a tag, so a long-running repo doesn't accumulate hundreds of stale lifecycle tags.

```json
{
"auto_tag": {
"enabled": true,
"baseline_format": "idd-{N}-baseline",
"verified_format": "idd-{N}-verified",
"push_remote": "origin"
}
}
```

| Field | Default | Meaning |
|-------|---------|---------|
| `enabled` | `true` | Master switch. **Default-ON**: with no config, `idd-issue` / `idd-verify` create + push these tags automatically. Set `enabled: false` to opt out completely. |
| `baseline_format` | `idd-{N}-baseline` | Tag placed by `idd-issue` on the local default-branch HEAD at issue-open time (the rollback anchor). `{N}` = issue number. |
| `verified_format` | `idd-{N}-verified` | Tag placed by `idd-verify` on the Aggregate-PASS snapshot (PR head in PR mode, current HEAD in local mode). |
| `push_remote` | `origin` | Remote the tags are pushed to. |

**Side-effect note (important).** Tag creation includes a `git push` to `push_remote` — a **repo-wide side effect** visible to everyone with the remote. Because the default is **ON**, a fresh IDD user gets `idd-{N}-baseline` / `idd-{N}-verified` tags on their remote without asking; `enabled: false` is the one-line opt-out. Tagging is **idempotent** (a tag that already exists is skipped, so re-runs never re-tag) and **graceful-skip** (a push failure on a fork / permission-denied / rejected remote warns and continues — it never aborts the `idd-issue` / `idd-verify` workflow). Intermediate lifecycle steps (`diagnose` / `plan` / `implement`) are deliberately **not** tagged; phase-level tagging stays manual.

## Resolution algorithm (canonical)

```
Expand Down
72 changes: 72 additions & 0 deletions plugins/issue-driven-dev/scripts/tests/auto-tag/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/usr/bin/env bash
# test.sh — drift-guard for selective git auto-tag (#85).
#
# #85 adds selective git-tag automation at two IDD milestones:
# - idd-issue creates `idd-{N}-baseline` (main HEAD at issue-open = rollback anchor)
# - idd-verify creates `idd-{N}-verified` on Aggregate PASS (review snapshot)
# opt-out via config `auto_tag.enabled`, idempotent, graceful-skip on push failure.
#
# WHY A CONTENT DRIFT-GUARD: the tag behaviors live in prose SKILL.md files
# (idd-issue Step 0/creation, idd-verify Aggregate-PASS branch) + a config schema
# (config-protocol.md) — AI prompts, not executable code. The falsifiable
# equivalent is asserting each file encodes the tag contract. The schema is
# documented across files that MUST agree; the failure mode is DRIFT (rename a
# format key in the schema, forget the SKILL that references it). Needles below
# are distinctive to #85 (0-occurrence before the change), so each is a genuine
# drift signal.
#
# Usage: bash test.sh (exit 0 = all pass, 1 = any fail)

set -u

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PLUGIN_ROOT="$HERE/../../.."
PROTOCOL="$PLUGIN_ROOT/references/config-protocol.md"
ISSUE="$PLUGIN_ROOT/skills/idd-issue/SKILL.md"
VERIFY="$PLUGIN_ROOT/skills/idd-verify/SKILL.md"
README="$PLUGIN_ROOT/README.md"

HELPERS="$HERE/../../lib/assert-helpers.sh"
[ -f "$HELPERS" ] || { echo "✗ missing $HELPERS — cannot run suite" >&2; exit 1; }
. "$HELPERS"

assert_file_exists "config-protocol.md exists" "$PROTOCOL"
assert_file_exists "idd-issue SKILL.md exists" "$ISSUE"
assert_file_exists "idd-verify SKILL.md exists" "$VERIFY"
assert_file_exists "plugin README.md exists" "$README"

# ── config-protocol.md: the auto_tag schema (source of truth) ──
assert_output_grep "protocol: declares the auto_tag field section" "### \`auto_tag\` field" "$PROTOCOL"
assert_output_grep "protocol: auto_tag config key" "auto_tag" "$PROTOCOL"
assert_output_grep "protocol: enabled key (default-on / opt-out)" "enabled" "$PROTOCOL"
assert_output_grep "protocol: baseline_format key" "baseline_format" "$PROTOCOL"
assert_output_grep "protocol: verified_format key" "verified_format" "$PROTOCOL"
assert_output_grep "protocol: push_remote key" "push_remote" "$PROTOCOL"
assert_output_grep "protocol: baseline tag naming default" "idd-{N}-baseline" "$PROTOCOL"
assert_output_grep "protocol: verified tag naming default" "idd-{N}-verified" "$PROTOCOL"
# default-ON must be explicit + opt-out documented (user decision 2026-07-09)
assert_output_grep "protocol: default-on documented" "default" "$PROTOCOL"
assert_output_grep "protocol: opt-out documented" "enabled: false" "$PROTOCOL"
# tag push is a repo-wide side effect — must be surfaced so default-on isn't a surprise
assert_output_grep "protocol: push side-effect surfaced" "side effect" "$PROTOCOL"

# ── idd-issue: baseline tag after issue creation ──
assert_output_grep "idd-issue: tag_baseline bootstrap step" "tag_baseline" "$ISSUE"
assert_output_grep "idd-issue: baseline tag naming" "idd-{N}-baseline" "$ISSUE"
assert_output_grep "idd-issue: gated on auto_tag config" "auto_tag" "$ISSUE"
assert_output_grep "idd-issue: idempotent (tag exists → skip)" "idempotent" "$ISSUE"
assert_output_grep "idd-issue: graceful-skip on push failure" "graceful-skip" "$ISSUE"
# never abort the workflow on tag failure (warn-continue discipline)
assert_output_grep "idd-issue: tag failure never aborts" "never abort" "$ISSUE"

# ── idd-verify: verified tag on Aggregate PASS ──
assert_output_grep "idd-verify: tag_verified bootstrap step" "tag_verified" "$VERIFY"
assert_output_grep "idd-verify: verified tag naming" "idd-{N}-verified" "$VERIFY"
assert_output_grep "idd-verify: gated on auto_tag config" "auto_tag" "$VERIFY"
assert_output_grep "idd-verify: fires on Aggregate PASS" "Aggregate PASS" "$VERIFY"

# ── README: discoverability note ──
assert_output_grep "README: auto-tag convention documented" "auto_tag" "$README"
assert_output_grep "README: baseline/verified naming shown" "idd-{N}-baseline" "$README"

print_summary "auto-tag (#85)"
33 changes: 33 additions & 0 deletions plugins/issue-driven-dev/skills/idd-issue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ TaskCreate(name="attach_images", description="上傳圖片到 attachments releas
TaskCreate(name="create_milestone", description="來源為文件時自動建立 milestone 並指派(見 Step 4.5)")
TaskCreate(name="clarity_surface", description="Step 4.6: delegate to /idd-clarify $NEW_ISSUE_NUMBER per IC clarity axis (skip in --multi-finding mode); failure → emit 'deferred' placeholder block + continue to Step 4.7 (per #135 v4 composable primitive design)")
TaskCreate(name="linked_context_sister_sweep", description="Step 4.7: scan body draft + linked attachments + recent session conversation for sibling-concern markers (also / additionally / 另外 / 順便 / etc); if hits AskUserQuestion 3-option per canonical references/ic-r011-checkpoint.md; PATCH just-created issue body with `### Linked-Context Siblings Filed` audit trail (advisory, non-blocking, per IC_R011 #529)")
TaskCreate(name="tag_baseline", description="Step 3.6 (#85): if config auto_tag.enabled (default on), tag idd-{N}-baseline at the local default-branch HEAD (rollback anchor) + git push push_remote. Idempotent (tag exists → skip + note); graceful-skip on push failure (never abort). Skip when auto_tag.enabled=false.")
TaskCreate(name="report_and_stop", description="回報 issue number/URL(group 模式列全部 + cross-link),停下等使用者決定下一步")
```

Expand Down Expand Up @@ -963,6 +964,38 @@ AskUserQuestion:

- [~] **Native relationship suggestion (deferred)** — body 內 `#N` 用 GitHub 2024+ GraphQL(`addBlockedByDependency` 等)建 structured relationship,比 body text reference 多 sidebar backlink。**本 cluster 刻意不做**,理由:(1) 複雜度/風險最高(需 relationship-type picker UI);(2) #141 spec 自身將其框為「reuse v2.52.0+ `--blocked-by` code path」的後續工作,而該 path 尚未一般化成可獨立呼叫的 primitive;(3) design Q4 裁定 body text reference 本來就保留(與 native backlink 互補不重複),所以延後不損失現有 inline context。點亮條件:`--blocked-by` 的 GraphQL mutation 抽成可重用 helper 後,再接本 (d)。

### Step 3.6: Baseline auto-tag(rollback anchor,v2.94.0+,#85)

Issue 建立成功、拿到 `$NUMBER` 之後,`tag_baseline` step 在當前 repo 打一個 **rollback baseline** tag,標記「issue 開案當下的 main HEAD」,方便日後一秒切回開工前的乾淨狀態(`git checkout idd-{N}-baseline`),不用記 commit hash。

**Config gate(預設 on,可 opt-out)** — 讀 walked-up config 的 `auto_tag`(schema 見 [`references/config-protocol.md`](../../references/config-protocol.md#auto_tag-field)):

```bash
AUTO_TAG_ENABLED=$(jq -r '.auto_tag.enabled // true' "$CONFIG_PATH" 2>/dev/null || echo true)
[ "$AUTO_TAG_ENABLED" = "false" ] && { echo "→ auto_tag disabled — skipping baseline tag"; } # opt-out: skip entirely
```

啟用時:

```bash
BASELINE_FMT=$(jq -r '.auto_tag.baseline_format // "idd-{N}-baseline"' "$CONFIG_PATH" 2>/dev/null || echo "idd-{N}-baseline")
PUSH_REMOTE=$(jq -r '.auto_tag.push_remote // "origin"' "$CONFIG_PATH" 2>/dev/null || echo origin)
TAG="${BASELINE_FMT/\{N\}/$NUMBER}" # idd-{N}-baseline → idd-42-baseline
DEFAULT_BRANCH=$(gh repo view "$GITHUB_REPO" --json defaultBranchRef -q .defaultBranchRef.name)

# idempotent: a tag that already exists is skipped (re-running idd-issue never re-tags)
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
echo "→ $TAG already exists — skip (idempotent)"
else
# baseline = the LOCAL default-branch HEAD (where main was when the issue opened)
git tag "$TAG" "$DEFAULT_BRANCH" && git push "$PUSH_REMOTE" "$TAG" \
&& echo "→ tagged $TAG at $DEFAULT_BRANCH HEAD + pushed to $PUSH_REMOTE" \
|| echo "⚠ auto_tag baseline: git tag/push failed (fork / no push permission / rejected) — graceful-skip, continuing"
fi
```

**鐵律 — graceful-skip,never abort**:tag 或 push 失敗(fork 無 push 權限、remote reject、default-branch 解析失敗等)**只 warn,絕不 abort** `idd-issue`(per IDD warn-continue pattern)。tag 是便利 checkpoint,不是 issue 建立的成敗條件。fork-aware:tag 打在當前 cwd 的 repo(Step 0.5 解析結果),push 到 `push_remote`(預設 `origin`)。intermediate step(diagnose / plan / implement)不 tag;phase-level tag 留手動。

### Step 4: 附加所有原始素材(鐵律:預設全保留)

> 引用 Step 1 的「資料保留鐵律」:來源中**任何附件都要全部上傳**,不論張數、不論格式。
Expand Down
36 changes: 36 additions & 0 deletions plugins/issue-driven-dev/skills/idd-verify/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ TaskCreate(name="recovery_protocol", description="Step 2.5 (NEW per #52): 缺 fi
TaskCreate(name="wait_for_codex", description="等 Codex 背景任務完成,讀 /tmp/codex-verify-${NUMBER}.md")
TaskCreate(name="merge_findings", description="合併 6 個來源 findings 去重,severity 取最高")
TaskCreate(name="post_master_and_pointers", description="PR mode: master 貼到 PR + capture URL → 為每個 ref'd issue 貼 pointer comment;本地 mode: 貼到 issue(單 issue 直接貼/多 issue 用 SOP master+pointer)")
TaskCreate(name="tag_verified", description="Step 4.5 (#85): if config auto_tag.enabled (default on) AND Aggregate PASS, tag idd-{N}-verified at the PR head (PR mode) / current HEAD (local mode) + git push. Idempotent + graceful-skip. Cluster: tag each ref'd #N. Skip on FAIL or auto_tag.enabled=false.")
TaskCreate(name="restore_working_tree", description="PR mode 結束後 git checkout 回原 branch(Step 0.5 記住的)")
TaskCreate(name="decide_next_action", description="根據 findings: 通過→idd-close / 有 findings→修正 / scope creep→新 issue")
TaskCreate(name="triage_followup_issues", description="Step 5b: 分類 non-blocking findings → 問使用者要不要開新 issue,確認後批次建立")
Expand Down Expand Up @@ -911,6 +912,41 @@ Verified scope: #98, #105
| 2 | P3 | ... | agents:security | Follow-up |
```

### Step 4.5: Verified auto-tag(review snapshot,v2.94.0+,#85)

Master report 貼出、且 **Aggregate PASS** 後,`tag_verified` step 在 review-ready 的 snapshot 打 `idd-{N}-verified` tag —— 一個 stable handle,日後可直接 `git checkout idd-{N}-verified` 切回「verify 通過那一刻」的 code,或在 PR / issue comment 引用給 reviewer。**只在 PASS 打**(FAIL 不 tag —— snapshot 的價值是「這是驗過的版本」)。

**Config gate(同 idd-issue baseline,預設 on)** — 讀 `auto_tag`(schema 見 [`references/config-protocol.md`](../../references/config-protocol.md#auto_tag-field)):

```bash
# Only when Aggregate verdict == PASS
[ "$AGGREGATE_VERDICT" != "PASS" ] && { echo "→ verify FAIL — no verified tag"; } # FAIL: no snapshot tag
# resolve the walked-up IDD config (new path first, legacy fallback — see the Configuration section)
CONFIG_PATH=$(d="$PWD"; while [ "$d" != / ]; do for f in "$d/.claude/.idd/local.json" "$d/.claude/issue-driven-dev.local.json"; do [ -f "$f" ] && { echo "$f"; break 2; }; done; d=$(dirname "$d"); done)
AUTO_TAG_ENABLED=$(jq -r '.auto_tag.enabled // true' "$CONFIG_PATH" 2>/dev/null || echo true)
[ "$AUTO_TAG_ENABLED" = "false" ] && { echo "→ auto_tag disabled — skipping verified tag"; } # opt-out

VERIFIED_FMT=$(jq -r '.auto_tag.verified_format // "idd-{N}-verified"' "$CONFIG_PATH" 2>/dev/null || echo "idd-{N}-verified")
PUSH_REMOTE=$(jq -r '.auto_tag.push_remote // "origin"' "$CONFIG_PATH" 2>/dev/null || echo origin)
# snapshot = HEAD. tag_verified runs BEFORE restore_working_tree, so in PR mode
# HEAD is still the PR head (Step 0.5 already ran `gh pr checkout`); in local
# mode (--commits / --since) HEAD is the tree just verified. Either way = HEAD.
SNAPSHOT=HEAD

for N in $ISSUE_NUMBERS; do # cluster: tag each ref'd issue (#N set from Step 0.5)
TAG="${VERIFIED_FMT/\{N\}/$N}" # idd-{N}-verified → idd-42-verified
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
echo "→ $TAG already exists — skip (idempotent)" # re-verify never re-tags
else
git tag "$TAG" "$SNAPSHOT" && git push "$PUSH_REMOTE" "$TAG" \
&& echo "→ tagged $TAG at verified snapshot + pushed" \
|| echo "⚠ auto_tag verified: git tag/push failed — graceful-skip, continuing"
fi
done
```

**鐵律 — graceful-skip,never abort**:同 baseline —— tag/push 失敗只 warn,絕不 abort verify(結果已 post,tag 是附加便利)。**Aggregate PASS 是唯一觸發條件**;FAIL run 不留 verified tag,避免把未過的 code 標成「已驗證」。cluster mode(多 `#N`)每個 issue 各打自己的 `idd-{N}-verified`。

### Step 5: 後續動作

#### Step 5a: 分類 findings
Expand Down
Loading