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
23 changes: 21 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
{
"name": "parallel-ai-agents",
"source": "./plugins/parallel-ai-agents",
"description": "v2.20.1: codex-call 補上 SSE error 事件的 message 提取路徑 (#25) — 直接呼叫 codex-call 時,HTTP 200 stream 內帶 message 的後端錯誤(如 server_is_overloaded)會顯示真實原因而非籠統的 \"Codex error\";經 ensemble 使用時仍受 #27 限制(消費端硬編碼失敗訊息)。v2.20.0: first-party codex-pro governance deep-integration (#23). v2.19.0: codexModel/codexEffort contract args (#22, caller-governed cross-model leg). 平行派發任務給多個 AI agent(Claude + Codex),獨立執行後交叉比對結果。Codex 改走直接 HTTP wrapper(bin/codex-call,Swift script)取代 codex exec subprocess,解決 hang 問題且避開 Python 版本飄移",
"version": "2.20.1",
"description": "v2.21.0: 三層 lens 疊加 built-in → lens pack → user (#29) — lens 可由外部 pai-lenses plugin 與 ~/.claude/pai-lenses/ 疊加,新增一條 lens 從「改 JS + bump plugin」降為「改 CSV」;撞名需顯式 override,報表附 provenance 行。v2.20.1: codex-call 補上 SSE error 事件的 message 提取路徑 (#25)。v2.20.0: first-party codex-pro governance deep-integration (#23). v2.19.0: codexModel/codexEffort contract args (#22, caller-governed cross-model leg). 平行派發任務給多個 AI agent(Claude + Codex),獨立執行後交叉比對結果。Codex 改走直接 HTTP wrapper(bin/codex-call,Swift script)取代 codex exec subprocess,解決 hang 問題且避開 Python 版本飄移",
"version": "2.21.0",
"author": {
"name": "Che Cheng"
},
Expand All @@ -22,6 +22,25 @@
"claude",
"codex"
]
},
{
"name": "pai-lenses",
"source": {
"source": "github",
"repo": "PsychQuant/pai-lenses"
},
"description": "parallel-ai-agents 的 lens pack(層 ②):以 CSV 提供可疊加的 reviewer lens。裝了之後四個 ensemble skill 的 lens 集合會自動疊上;撞名需在 CSV 標 override 才取代。新增一條 lens = 改 CSV + bump 版本,不必動 plugin 程式碼。",
"version": "0.1.0",
"author": {
"name": "Che Cheng"
},
"category": "development",
"keywords": [
"parallel-ai-agents",
"ensemble-review",
"lenses",
"code-review"
]
}
]
}
16 changes: 15 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,21 @@ jobs:
run: shellcheck bin/pai-build-diff bin/pai-parse-verdict bin/pai-iter-commit

- name: py_compile
run: python3 -m py_compile bin/pai-parse-lens-csv
run: python3 -m py_compile bin/pai-parse-lens-csv bin/pai-collect-lens-layers

# builtin-lenses.csv is generated from the harness PROFILES. It drives nothing at runtime
# (#29 keeps the built-in baseline inside the harness), so a stale catalog is a DOCS defect,
# not a correctness one — but this repo's pitch is "contribute a lens", and a catalog that
# disagrees with the code sends contributors at the wrong thing. Three lines to prevent that.
- name: builtin-lenses.csv drift (regenerate → expect no diff)
shell: bash
run: |
set -o pipefail
bash references/regen-builtin-lenses.sh
git diff --exit-code -- references/builtin-lenses.csv || {
echo "::error::references/builtin-lenses.csv is stale — run 'bash references/regen-builtin-lenses.sh' and commit the result"
exit 1
}

- name: bats
run: bats test/
Expand Down
2 changes: 1 addition & 1 deletion plugins/parallel-ai-agents/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "parallel-ai-agents",
"description": "v2.20.1: codex-call 補上 SSE error 事件的 message 提取路徑 (#25) — 直接呼叫 codex-call 時,HTTP 200 stream 內帶 message 的後端錯誤(如 server_is_overloaded)會顯示真實原因而非籠統的 \"Codex error\";經 ensemble 使用時仍受 #27 限制(消費端硬編碼失敗訊息)。v2.20.0: first-party skills deep-integrate codex-pro governance (#23, mirroring issue-driven-dev#264) — new references/codex-governance.md (canonical resolution: MIN_CODEX_PRO 0.7.0 gate, defaults.json base + two profile.yaml layers, fail-fast with install instruction when codexEnabled and codex-pro absent); ensemble-code-review / ensemble-academic-review / ensemble-compose(--codex) resolve and pass codexModel/codexEffort explicitly; engine + bin/codex-call baked defaults become release-time governance SNAPSHOTS (bumped to gpt-5.6-sol) — authoritative source is codex-pro's defaults.json; all first-party prose generation-neutral. v2.19.0: codexModel / codexEffort engine args (#22) — the cross-model codex leg's model and effort become caller-governed contract args (defaults gpt-5.5 / xhigh preserve pre-#22 behavior byte-identically). First consumer: issue-driven-dev passing codex-pro-resolved governance. 平行派發任務給多個 AI agent(Claude + Codex),獨立執行後交叉比對結果。Codex 改走直接 HTTP wrapper(bin/codex-call,Swift script)取代 codex exec subprocess,解決 hang 問題且避開 Python 版本飄移",
"version": "2.20.1",
"version": "2.21.0",
"author": {
"name": "Che Cheng"
}
Expand Down
22 changes: 22 additions & 0 deletions plugins/parallel-ai-agents/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [2.21.0] - 2026-08-01

### Added

- **三層 lens 疊加:built-in → lens pack → user (#29)** — lens 集合不再只能來自 harness 的 `PROFILES`。`pai-lenses` plugin 的 `lenses/<profile>.csv`(層 ②)與 `~/.claude/pai-lenses/<profile>.csv`(層 ③)會自動疊上來。新增一條 lens 的成本從「改 JS + bump plugin + 同步 marketplace」降為「改 CSV + bump lens pack」;外部貢獻的**出口成本**同步下降 —— 收一條 lens 不再等於發一次 plugin release。設計見 `docs/superpowers/specs/2026-07-29-lens-pack-externalization-design.md`(D1–D8),契約見 `references/lens-layers.md`。
- **`bin/pai-collect-lens-layers`** — 層 ②③ 的蒐集器(跨 marketplace semver glob 定位 lens pack、委派 `pai-parse-lens-csv` 解析、依序串接)。四個 ensemble skill 共用同一個入口。
- **`stats.lensProvenance`** — harness 回報每個 lens 的處置(`added` / `overridden` / `ignored` + `overrodeFrom`),供報表的 provenance 行使用。
- **報表新增 provenance 行** — 列出各層來源與版本、哪些 lens 被覆蓋。**沒裝 lens pack 時也會印**:量測儀器換了刻度卻不說,是 eval 偵測率數字前後不可比的根源。
- **CSV 新增可選欄 `override`** — truthy(`1`/`true`/`yes`)時取代同 key 的既有 lens。語意是「我要取代那一條」,不是「我比較重要」。
- **CI 新增 `builtin-lenses.csv` drift 檢查** — 跑 regen 後 `git diff --exit-code`。catalog 過期是文件缺陷(它不驅動 runtime),但會把想貢獻 lens 的人指向錯的檔案。

### Changed

- **harness 的 lens 去重從純 first-wins 改為 override-aware** — 撞名時後來者只有標了 `override` 才勝出,且是**原位取代**(devil's-advocate 依 lens 順序讀 reviewer 完稿,移位會讓它看到的東西因與 override 無關的理由改變)。**未標記的行為與 2.20.1 逐位元相同**,向後相容鎖有專屬測試。
- **`references/builtin-lenses.csv` 檔頭標明唯讀** 並指向 lens pack。註解列刻意放在 header **之後** —— 放前面會被 `csv.DictReader` 當成 header,整份檔案解析成空。

### Known limitations

- **`profile.title` 沒有 `args` 覆寫路徑** —— 這是為何三個 profile skill 的 `profile` 必須維持原值而非改傳 `"custom"`(改了會讓每次審閱對所有 agent 自稱「自訂 ensemble」)。目前無驅動案例要求可覆寫。
- **專案級 lens(第四層 `.claude/pai-lenses/`)未實作** —— 折疊形式已使增層只需延長來源序列,但無需求驅動(spec §11 明確排除)。
- **多個 lens pack 並存的優先序未定義** —— 本版假設單一 pack;`pai-collect-lens-layers` 取 semver 最高者。

## [2.20.1] - 2026-07-31

### Fixed
Expand Down
158 changes: 158 additions & 0 deletions plugins/parallel-ai-agents/bin/pai-collect-lens-layers
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#!/usr/bin/env python3
"""pai-collect-lens-layers — 蒐集三層 lens 疊加的層 ②(lens pack)與層 ③(user)。

層 ① built-in **不在這裡**:它活在 harness 的 `PROFILES` 物件裡,而 workflow runtime
沒有檔案系統存取,所以 built-in 永遠由 harness 自己供給。這支只負責「skill 讀得到、
harness 讀不到」的那兩層,結果由 skill 併進 `args.customLenses`。

用法:pai-collect-lens-layers <profile>

輸出 JSON 到 stdout:
{
"lenses": [{"key","focus","needsSrt"?,"override"?,"_layer":"pack"|"user"}, ...],
"layers": [{"name","path","version","status"}, ...],
"warnings": [...]
}

陣列順序即優先序:pack 在前、user 在後。撞名的判定(first-wins,除非標 `override`)
**全部交給 harness** —— 這裡只搬運、不去重。單一去重點才不會兩邊語意漂移(#29)。

CSV 解析一律委派 `bin/pai-parse-lens-csv`(BOM-safe、csv 模組、bats 覆蓋的單一真相源)。

status 語意(對應 spec D5「缺席與損壞分開處理」):
absent 沒裝 → 靜默,不產生 warning
ok 正常
empty 檔案在、卻解析出 0 條 lens(多半是 header 打錯)→ 警告。
這是最危險的失敗模式:不警告的話它會安靜地什麼都不貢獻
corrupt 解析器非零退出(讀不到 / 編碼壞)→ 警告
unversioned 裝了 pai-lenses 卻沒有 semver 目錄(plugin.json 缺 version)→ 警告。
「裝了但不可用」不是「沒裝」,不可靜默

環境變數(測試注入用;預設值即正式路徑):
PAI_LENS_CACHE_ROOT 預設 ~/.claude/plugins/cache
PAI_USER_LENS_DIR 預設 ~/.claude/pai-lenses

退出碼:0(含全部缺席);2 用法錯。
"""
import json
import os
import re
import subprocess
import sys
from pathlib import Path

PACK_PLUGIN = "pai-lenses"
SEMVER = re.compile(r"^(\d+)\.(\d+)\.(\d+)")
PARSER = Path(__file__).resolve().parent / "pai-parse-lens-csv"


def _semver_key(name):
m = SEMVER.match(name)
return tuple(int(g) for g in m.groups()) if m else None


def find_pack_dir(cache_root):
"""回傳 (dir, version, status)。跨 marketplace glob —— lens pack 註冊在哪個
marketplace 是使用者端的事,不該寫死在這裡。"""
candidates = []
homes = sorted(Path(cache_root).glob(f"*/{PACK_PLUGIN}")) if cache_root.exists() else []
for home in homes:
if not home.is_dir():
continue
for child in home.iterdir():
if not child.is_dir():
continue
key = _semver_key(child.name)
if key is not None:
candidates.append((key, child))
if candidates:
key, path = max(candidates, key=lambda kv: kv[0])
return path, ".".join(str(n) for n in key), "ok"
# 有 pai-lenses 目錄卻挑不出 semver 版本 = 裝了但不可用,不是沒裝
if homes:
return None, None, "unversioned"
return None, None, "absent"


def parse_csv(path):
"""→ (lenses, status, warning)。解析委派給 pai-parse-lens-csv。"""
if not path or not path.is_file():
return [], "absent", None
proc = subprocess.run(
[sys.executable, str(PARSER), str(path)],
capture_output=True, text=True,
)
if proc.returncode != 0:
return [], "corrupt", f"lens CSV 解析失敗({path}):{proc.stderr.strip()}"
try:
lenses = json.loads(proc.stdout)
except json.JSONDecodeError as e:
return [], "corrupt", f"lens CSV 解析輸出不是 JSON({path}):{e}"
if not lenses:
return [], "empty", (
f"lens CSV 解析出 0 條 lens({path})—— header 是否為 key,focus?"
"檔案存在卻不貢獻任何 lens,多半是打錯而不是刻意留空。"
)
return lenses, "ok", None


def main():
if len(sys.argv) != 2:
print("用法:pai-collect-lens-layers <profile>", file=sys.stderr)
return 2
profile = sys.argv[1].strip()
if not profile:
print("用法:pai-collect-lens-layers <profile>", file=sys.stderr)
return 2

cache_root = Path(os.environ.get("PAI_LENS_CACHE_ROOT")
or Path.home() / ".claude" / "plugins" / "cache")
user_dir = Path(os.environ.get("PAI_USER_LENS_DIR")
or Path.home() / ".claude" / "pai-lenses")

lenses, layers, warnings = [], [], []

# ── 層 ②:lens pack ──
pack_dir, pack_version, pack_status = find_pack_dir(cache_root)
pack_csv = pack_dir / "lenses" / f"{profile}.csv" if pack_dir else None
if pack_status == "unversioned":
warnings.append(
f"{PACK_PLUGIN} 已安裝但找不到 semver 版本目錄({cache_root})—— "
"lens pack 的 plugin.json 必須有 version 欄位 —— 缺了的話 cache 目錄名會退回 "
"git commit SHA(官方文件的 fallback)或 unknown(實測見過),兩者都不是 semver、"
"定位不到。本層已略過。"
)
elif pack_status == "ok":
got, csv_status, warn = parse_csv(pack_csv)
pack_status = csv_status
lenses += [dict(l, _layer="pack") for l in got]
if warn:
warnings.append(warn)
layers.append({
"name": "pack",
"path": str(pack_csv) if pack_csv else None,
"version": pack_version,
"status": pack_status,
})

# ── 層 ③:user ──
user_csv = user_dir / f"{profile}.csv"
got, user_status, warn = parse_csv(user_csv)
lenses += [dict(l, _layer="user") for l in got]
if warn:
warnings.append(warn)
layers.append({
"name": "user",
"path": str(user_csv),
"version": None,
"status": user_status,
})

json.dump({"lenses": lenses, "layers": layers, "warnings": warnings},
sys.stdout, ensure_ascii=False)
sys.stdout.write("\n")
return 0


if __name__ == "__main__":
sys.exit(main())
19 changes: 14 additions & 5 deletions plugins/parallel-ai-agents/bin/pai-parse-lens-csv
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@

用 csv 模組(絕不 naive split —— focus 是含逗號/中文標點的長 prose)。讀 UTF-8(含 BOM)
CSV,每列 = 一個自訂 lens,輸出 JSON array 到 stdout:
[{"key": "...", "focus": "...", "needsSrt": true?}, ...]
[{"key": "...", "focus": "...", "needsSrt": true?, "override": true?}, ...]

契約:
- header 需含 key, focus(needsSrt 可選欄
- header 需含 key, focus(needsSrt / override 為可選欄
- 跳過 key 或 focus 為空的列(與 harness 的 customLenses filter 對稱)
- needsSrt ∈ {1,true,yes}(不分大小寫)→ true;否則省略該欄
- 保留列序、不去重(跨來源 first-wins 去重由 harness 做)
- needsSrt / override ∈ {1,true,yes}(不分大小寫)→ true;否則省略該欄
- 保留列序、不去重(跨來源去重由 harness 做:預設 first-wins,標了
override 的列才取代同 key 的既有 lens —— 見 #29)
- 檔案不存在/不可讀 → stderr + exit 1

退出碼:0 成功(含空 array);1 錯誤;2 用法錯。
Expand All @@ -19,6 +20,12 @@ import json
import sys


def _truthy(value):
"""CSV 布林欄的共用判準(needsSrt / override)—— 兩欄必須同一套語意,
否則寫 lens pack 的人得記兩組規則。"""
return str(value or "").strip().lower() in ("1", "true", "yes")


def main():
if len(sys.argv) != 2:
print("用法:pai-parse-lens-csv <csv-path>", file=sys.stderr)
Expand All @@ -35,8 +42,10 @@ def main():
if not key or not focus:
continue
item = {"key": key, "focus": focus}
if str(row.get("needsSrt", "")).strip().lower() in ("1", "true", "yes"):
if _truthy(row.get("needsSrt")):
item["needsSrt"] = True
if _truthy(row.get("override")):
item["override"] = True
out.append(item)
except FileNotFoundError:
print(f"CSV 檔不存在: {path}", file=sys.stderr)
Expand Down
1 change: 1 addition & 0 deletions plugins/parallel-ai-agents/references/builtin-lenses.csv
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
profile,key,focus,needsSrt
# 唯讀 catalog — 編輯本檔不會改變任何行為。真源是 workflows/ensemble-workflow.js 的 PROFILES。要新增/修改 lens 請走 lens pack 或 ~/.claude/pai-lenses/(見 references/lens-layers.md)。本檔由 references/regen-builtin-lenses.sh 產生。
lecture,content-accuracy,"教學講義的知識正確性。檢查:(1) 統計概念定義是否正確(p-value、power、effect size、confidence interval);(2) 公式/數學符號有無寫錯(KaTeX 語法);(3) 心理學理論描述是否準確(如 Higgins, Regulatory Focus/Fit);(4) 因果推論有無把相關說成因果或過度推論;(5) 術語一致性(同概念是否同名);(6) 範例是否恰當支持概念。",false
lecture,student-readability,"從「零程式基礎」學生角度的易懂程度。檢查:(1) 白話程度(專業術語有無解釋);(2) 邏輯銜接(段落跳躍是否太大);(3) 抽象概念有無搭配具體例子;(4) 表格/圖表是否幫助理解(還是增加混淆);(5) 篇幅平衡(重要概念是否足夠篇幅);(6) 結構導航(標題是否清楚、好找);(7)「重點整理」是否真涵蓋最重要內容。自問:「零基礎學生讀到這裡會卡住嗎?」",false
lecture,completeness,"講義是否完整覆蓋上課內容。檢查:(1) 逐字稿覆蓋率(逐字稿有教但講義漏寫的重點,最重要);(2) 結構完整性(有無「重點整理」「課後作業」section;h2/h3 層級正確、無孤立 h3;h2 之間有 --- 分隔);(3) KaTeX/Mermaid(有數學符號處有無 KaTeX CDN、有路徑圖處有無 Mermaid 或 ASCII art);(4) 連結有效性(href 指向的檔案是否存在);(5) 缺少的教學元素(該有 blockquote 提醒、該用表格對比卻只用文字處)。",true
Expand Down
Loading
Loading