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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions plugins/issue-driven-dev/references/config-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -433,14 +433,24 @@ If a group's `when` matches AND a candidate's `when` matches, **groups take prec

"ask_each_time": false, // OPTIONAL. If true and candidates/groups exist, always prompt.

"pr_policy": "ask" // OPTIONAL. "always" | "never" | "ask" (default).
"pr_policy": "ask", // OPTIONAL. "always" | "never" | "ask" (default).
// Controls idd-implement PR vs direct-commit path.
// Fork detection always overrides to "always".
// See references/pr-flow.md for full algorithm.

"collaborators": [ // OPTIONAL. Identity registry — resolve a person's
{ // alias / email / display-name → GitHub @login WITHOUT guessing.
"github_login": "hardy1yang", // REQUIRED. Canonical handle — the only string GitHub notifies.
"display_name": "Hau-Hung Yang", // REQUIRED. Real name (may be 中文), echoed back on resolve.
"role": "collaborator", // OPTIONAL. maintainer | collaborator | advisor | external.
"aliases": ["hardy", "楊浩弘", "s1093301"],// OPTIONAL. Nicknames / student IDs / romanizations for fuzzy match.
"email": "hardy@example.edu" // OPTIONAL, PII — private/gitignored config ONLY, never committed/public.
}
]
}
```

**Backward compatibility**: configs without `candidates` / `groups` / `ask_each_time` / `pr_policy` work exactly as before — they're plain single-target configs. All new fields are additive.
**Backward compatibility**: configs without `candidates` / `groups` / `ask_each_time` / `pr_policy` / `collaborators` work exactly as before — they're plain single-target configs. All new fields are additive.

### `pr_policy` field

Expand All @@ -456,6 +466,20 @@ Controls whether `idd-implement` opens a PR or commits directly.

`idd-all` always enforces `--pr` regardless of `pr_policy`. Full path contract: [pr-flow.md](pr-flow.md).

### `collaborators[]` field

An OPTIONAL identity registry so IDD can resolve a person's alias / email / display-name → their GitHub `@login` **without guessing** — the hard rule set by [tagging-collaborators.md](../rules/tagging-collaborators.md). It is a *resolution accelerator*, **not** an authority: a table hit is still existence-verified via `gh api users/<login>` before any mention is posted, because the table can go stale.

| Field | Req? | Meaning |
|-------|------|---------|
| `github_login` | ✅ | Canonical GitHub handle — the only string GitHub actually notifies. Charset `A-Za-z0-9-`. |
| `display_name` | ✅ | Real name (may be 中文), echoed back to the user on resolve as a sanity check. |
| `role` | — | `maintainer` \| `collaborator` \| `advisor` \| `external`. Informational only. |
| `aliases` | — | Nicknames / student IDs / romanizations for fuzzy input matching. Unique across the whole registry. |
| `email` | — | **PII.** See boundary below. |

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

## Resolution algorithm (canonical)

```
Expand Down
13 changes: 13 additions & 0 deletions plugins/issue-driven-dev/rules/tagging-collaborators.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,19 @@ git log --pretty=format:'%an <%ae>' | sort -u > /tmp/idd-commit-authors.txt
- Handles inferred from git config / email domains
- Handles from `~/.gitconfig` / `~/.ssh/config` / `gh auth status`

### Step 2.5: Consult the config registry first (acceleration, not authority)

If the walked-up IDD config has a `collaborators[]` array (schema in [references/config-protocol.md](../references/config-protocol.md)), use it as the **first** resolution attempt — it carries the human's own curated alias/name → `@login` mapping, so it resolves `Hardy` / `楊浩弘` / a student ID that the raw API list can't. Match the input, in priority order:

1. `github_login` exact (case-insensitive)
2. `aliases[]` exact (case-insensitive)
3. `email` exact — **only if** the input literally is an email
4. `display_name` substring

On a **hit**, resolve to that entry's `github_login` — **but the table is an accelerator, never an authority.** It can go stale (a collaborator removed, a login renamed after the config was written), so a hit MUST still be existence-verified via `gh api users/<login>` before it counts as resolved and before `--mention-attested` is passed. A hit that fails existence-verification falls through to Step 3 (treat as no config match).

On a **miss** (no `collaborators[]`, or no entry matches), fall through to Step 3 and fuzzy-match against the API-fetched list from Step 2 as before. The registry never replaces Step 2's fetch — it only front-runs the resolution when it can.

### Step 3: Resolve user input → @login

Apply fuzzy matching against the real list:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# test.sh — schema-consistency drift guard for the collaborators[] identity
# registry (PsychQuant/issue-driven-development#86).
#
# #86 adds an OPTIONAL `collaborators[]` config array so IDD can resolve a
# person's alias / email / display-name → their GitHub @login WITHOUT guessing
# (feeds rules/tagging-collaborators.md Step 2-3 as a resolution accelerator).
# The schema is documented across THREE files that MUST agree:
# - references/config-protocol.md (schema source of truth + PII boundary)
# - rules/tagging-collaborators.md (consumer: table-lookup then verify)
# - skills/idd-config/SKILL.md (validate: schema checks)
#
# This is a C_shared_module_coord change, so the real failure mode is DRIFT:
# rename a field in one file, forget the other two, and the tagging protocol
# resolves against a stale schema. There is deliberately no behavioral test —
# the resolution itself is LLM-executed prose (consistent with the whole rules/
# corpus), so there is no resolver binary to unit-test. What IS mechanically
# checkable, and what breaks in practice, is that all three files still describe
# the same distinctive field set + the PII boundary. The needles below are
# 0-occurrence in all three files BEFORE #86 (verified), so each is a genuine
# drift signal, not a token that happens to pre-exist for another reason
# (`role`, e.g., pre-exists via groups[].repos[].role — deliberately not tested).
set -u

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$HERE/../../lib/assert-helpers.sh"
ROOT="$(cd "$HERE/../../.." && pwd)" # → plugins/issue-driven-dev

PROTOCOL="$ROOT/references/config-protocol.md"
TAGGING="$ROOT/rules/tagging-collaborators.md"
IDDCONFIG="$ROOT/skills/idd-config/SKILL.md"

echo "collaborators-schema (3-file drift guard, #86)"

# ── config-protocol.md — the schema source of truth ──
assert_output_grep "protocol declares the collaborators[] field" "collaborators[" "$PROTOCOL"
assert_output_grep "protocol: github_login (required @-handle)" "github_login" "$PROTOCOL"
assert_output_grep "protocol: display_name (required real name)" "display_name" "$PROTOCOL"
assert_output_grep "protocol: aliases (optional fuzzy-match keys)" "aliases" "$PROTOCOL"
assert_output_grep "protocol: PII boundary documented" "PII" "$PROTOCOL"

# ── tagging-collaborators.md — consumes the registry as a lookup accelerator ──
assert_output_grep "tagging references the collaborators[] registry" "collaborators[" "$TAGGING"
assert_output_grep "tagging resolves to github_login" "github_login" "$TAGGING"
# regression lock: a table HIT must STILL existence-verify (table can be stale).
assert_output_grep "tagging keeps the gh api users/ existence-verify" "users/" "$TAGGING"

# ── skills/idd-config/SKILL.md — validate schema-checks the registry ──
assert_output_grep "idd-config validate covers collaborators[]" "collaborators[" "$IDDCONFIG"
assert_output_grep "idd-config validate checks github_login format" "github_login" "$IDDCONFIG"
assert_output_grep "idd-config validate checks aliases uniqueness" "aliases" "$IDDCONFIG"
# the PII-reminder contract lives in idd-config too — keep it anchored so it
# can't be dropped from validate while config-protocol.md still advertises it.
assert_output_grep "idd-config validate carries the PII reminder" "PII" "$IDDCONFIG"

print_summary
3 changes: 3 additions & 0 deletions plugins/issue-driven-dev/skills/idd-config/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ Local-only(`TARGET=""`):不寫 `github_repo`,提示 GitHub-backed idd-*
```
TaskCreate(name="validate_load", description="讀 IDD config(.claude/.idd/local.json 優先,legacy 次之,#195),JSON parse")
TaskCreate(name="validate_schema", description="檢查 required fields + 各 candidates/groups 結構")
TaskCreate(name="validate_collaborators", description="若有 collaborators[]:github_login 必填+格式、display_name 必填、role enum、aliases 全域唯一;email 出現→PII warning(#86)")
TaskCreate(name="validate_repo_exists", description="對 github_repo / candidates[].github_repo / groups[].repos[].github_repo 跑 gh repo view 驗證實際存在")
TaskCreate(name="validate_predicate_form", description="when 區塊 path_contains / title_matches 等 key 是 known set")
TaskCreate(name="validate_report", description="輸出 PASS / list of issues")
Expand All @@ -272,6 +273,8 @@ TaskCreate(name="validate_report", description="輸出 PASS / list of issues")
- `github_repo` 形式:`owner/repo`(regex `^[\w\-\.]+/[\w\-\.]+$`)
- `candidates[].github_repo` 同上
- `groups[]` 必須有**剛好一個** `role: "primary"`
- `collaborators[]`(若存在,schema 見 [config-protocol.md](../../references/config-protocol.md)「`collaborators[]` field」):每個 entry `github_login` **必填**且符合 GitHub login charset `^[A-Za-z0-9-]+$`(明顯錯字如含空白/`@`/中文 → error);`display_name` **必填**非空;`role`(若有)∈ `{maintainer, collaborator, advisor, external}`(其餘 → warning);**`aliases` 全域唯一** —— 跨所有 entry 的 `aliases` + `github_login` 攤平後不得重複(否則 fuzzy match 會 ambiguous,這是 error)
- `collaborators[].email` 若出現 → **PII 提醒**(warning,非 error):email 是可識別個資,不該進 committed/public config,只放 private/gitignored config layer(見 config-protocol.md「PII boundary」)
- `when.path_contains` / `path_matches` / `title_matches` / `label_in` / `git_remote_matches` / `git_branch_matches` / `all` / `any` / `not` — 不認識的 key → warning(不 fail)
- `gh repo view` 對每個 repo 跑(warning 而非 error,因為 private repo 無權限會失敗但 config 本身可能合法)

Expand Down
Loading