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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"name": "parallel-ai-agents",
"source": "./plugins/parallel-ai-agents",
"description": "平行派發任務給多個 AI agent(Claude + Codex),獨立執行後交叉比對結果。Codex 改走直接 HTTP wrapper(bin/codex-call,Swift script)取代 codex exec subprocess,解決 hang 問題且避開 Python 版本飄移",
"version": "2.17.0",
"version": "2.18.0",
"author": {
"name": "Che Cheng"
},
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": "平行派發任務給多個 AI agent(Claude + Codex),獨立執行後交叉比對結果。Codex 改走直接 HTTP wrapper(bin/codex-call,Swift script)取代 codex exec subprocess,解決 hang 問題且避開 Python 版本飄移",
"version": "2.17.0",
"version": "2.18.0",
"author": {
"name": "Che Cheng"
}
Expand Down
7 changes: 7 additions & 0 deletions plugins/parallel-ai-agents/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [2.18.0] - 2026-07-02

### Added

- **Explicit dispatch model for every ensemble agent — default `opus` (#20, mirrors issue-driven-development#205)** — `ensemble-workflow.js` resolves `AGENT_MODEL` from the new `args.agentModel` (whitelist `sonnet|opus|haiku|fable`; absent → `opus`; an explicitly invalid value throws **before any dispatch**) and passes `model: AGENT_MODEL` at all 3 `agent()` sites (lens reviewers × replicas, the codex runner, devil's-advocate); `stats.dispatchModel` + the progress log disclose what actually ran. All 5 ensemble-* skills resolve `PAI_AGENT_MODEL` (unset → `opus`, invalid → usage-error abort) and pass it as `agentModel`; legacy TeamCreate fallback backends carry the same explicit model per spawned Agent. Rationale: an unpinned dispatch inherits the session's main-loop model — on high-tier sessions that burned 563k–1,092k tokens per ensemble round and killed a lens agent at a session limit (evidence in the primary issue). Regression tests: default-opus-everywhere, override honored, invalid-throws-before-dispatch (3 new cases in `test/ensemble-workflow.test.mjs`).
- **External-consumer contract officialized (#20)** — the engine's args surface + return shape are now the documented STABLE API for dependent plugins (first consumer: `issue-driven-development`'s idd-verify, which will swap its vendored 305-line fork for resolve-installed-engine dependency per the user's direct-dependency ruling). Breaking changes require a major bump + migration note.

## [2.17.0] - 2026-06-10

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,12 @@ TaskCreate: "Final: merge all rounds"
每一輪這樣呼叫:

1. 解析 harness 絕對路徑 `${CLAUDE_PLUGIN_ROOT}/workflows/ensemble-workflow.js`;wrapper 絕對路徑 `${CLAUDE_PLUGIN_ROOT}/bin/codex-call`。
2. 呼叫 `Workflow` tool,傳 `scriptPath` + `args`:
2. 解析 dispatch model(#20):`PAI_AGENT_MODEL` 未設 → `opus`;設了但不在 `sonnet|opus|haiku|fable` → **abort with usage error**(fail-loud,不靜默換模型;engine 對顯式非法值亦會於派發前 throw 作第二層)。解析值經 `args.agentModel` 傳入。接著呼叫 `Workflow` tool,傳 `scriptPath` + `args`:

```json
{
"profile": "academic",
"agentModel": "<PAI_AGENT_MODEL 解析值(預設 opus,#20)>",
"file": "<FILE 絕對路徑>",
"contextBlock": "<全文/文獻列表/ground-truth artifact 清單/focus>",
"codexEnabled": true,
Expand All @@ -215,6 +216,8 @@ TaskCreate: "Final: merge all rounds"

#### Backend B — Legacy TeamCreate + Codex Bash(fallback)

> 每個 spawn 的 Agent 都帶顯式 `model: $PAI_AGENT_MODEL`(預設 `opus`,#20——不繼承 session 主迴圈模型)。

**CRITICAL: 所有 tool calls(TeamCreate + Codex Bash)必須在同一個 message 送出。不可分步驟。**

**啟用的 reviewer 數依設定變動**:預設 5(含 number+reference verifier),加 `--no-numeric` 變 4,再加 `--no-references` 變 3。Codex 永遠跑。一個 message 內 = (啟用 reviewer 數) Agent + 1 Bash。
Expand All @@ -236,6 +239,7 @@ TeamCreate:
Agent:
name: "methodology"
subagent_type: "general-purpose"
model: "<PAI_AGENT_MODEL 解析值(預設 opus,#20)>"
team_name: "academic-review-{timestamp}-round{N}"
prompt: |
你是 Methodology Reviewer,專門審閱學術研究方法。
Expand All @@ -262,6 +266,7 @@ Agent:
Agent:
name: "writing"
subagent_type: "general-purpose"
model: "<PAI_AGENT_MODEL 解析值(預設 opus,#20)>"
team_name: "academic-review-{timestamp}-round{N}"
prompt: |
你是 Writing Quality Reviewer,專門審閱學術寫作品質。
Expand Down Expand Up @@ -290,6 +295,7 @@ Agent:
Agent:
name: "reference-verifier"
subagent_type: "general-purpose"
model: "<PAI_AGENT_MODEL 解析值(預設 opus,#20)>"
team_name: "academic-review-{timestamp}-round{N}"
prompt: |
你是 Reference Verifier,專門驗證學術文獻的真實性。
Expand Down Expand Up @@ -339,6 +345,7 @@ Agent:
Agent:
name: "number-verifier"
subagent_type: "general-purpose"
model: "<PAI_AGENT_MODEL 解析值(預設 opus,#20)>"
team_name: "academic-review-{timestamp}-round{N}"
prompt: |
你是 Number Verifier,專門驗證學術/實證文件中每一個數字 vs ground truth artifact。
Expand Down Expand Up @@ -396,6 +403,7 @@ Agent:
Agent:
name: "devils-advocate"
subagent_type: "general-purpose"
model: "<PAI_AGENT_MODEL 解析值(預設 opus,#20)>"
team_name: "academic-review-{timestamp}-round{N}"
prompt: |
你是 Devil's Advocate,學術審閱的對抗性驗證者。
Expand Down
13 changes: 12 additions & 1 deletion plugins/parallel-ai-agents/skills/ensemble-code-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,12 @@ esac

1. 解析 harness 絕對路徑:`${CLAUDE_PLUGIN_ROOT}/workflows/ensemble-workflow.js`。
2. 解析 wrapper 絕對路徑:`${CLAUDE_PLUGIN_ROOT}/bin/codex-call`(**用絕對路徑**,不賭 workflow agent shell 的 PATH —— install-time PATH 注入是 version-pinned、可能 stale/不存在)。
3. 呼叫 `Workflow` tool,傳 `scriptPath`(harness 絕對路徑)+ `args`:
3. 解析 dispatch model(#20):`PAI_AGENT_MODEL` 未設 → `opus`;設了但不在 `sonnet|opus|haiku|fable` → **abort with usage error**(fail-loud,不靜默換模型;engine 對顯式非法值亦會於派發前 throw 作第二層)。解析值經 `args.agentModel` 傳入。接著呼叫 `Workflow` tool,傳 `scriptPath`(harness 絕對路徑)+ `args`:

```json
{
"profile": "code",
"agentModel": "<PAI_AGENT_MODEL 解析值(預設 opus,#20)>",
"file": "<路徑模式:FILE_OR_DIR 絕對路徑>",
"diffFile": "<diff 模式:Phase 1 的 $DIFF_FILE 絕對路徑>",
"contextBlock": "<focus + 內容類型 emphasis(見 Phase 1 表)>",
Expand All @@ -157,6 +158,8 @@ esac

#### Backend B — Legacy TeamCreate + Codex Bash(fallback)

> 每個 spawn 的 Agent 都帶顯式 `model: $PAI_AGENT_MODEL`(預設 `opus`,#20——不繼承 session 主迴圈模型)。

> **diff 模式時**(不只換路徑字串):① 把下方 prompt 的 `審閱範圍:{FILE_OR_DIR}` 換成 `審閱範圍(diff):$DIFF_FILE`;② **在每個 reviewer prompt 開頭加一句框架引導**:「以下是一份 diff,只審變更行、評估**變更的影響面與回歸風險**;需要時自行 Read 周邊原始碼補 context」——否則 teammate 會用『審整棵原始碼樹』的 mental model 看 diff(如 architecture 的『檔案組織/死碼』對著一份 diff 語意走樣);③ TeamCreate 的 `description` 不要塞 temp 檔路徑,用「diff review」之類描述。devil's-advocate 走 SendMessage 不受影響。

**CRITICAL: 所有 tool calls(TeamCreate + Codex Bash)必須在同一個 message 送出。不可分步驟。**
Expand All @@ -180,8 +183,10 @@ TeamCreate:
Agent:
name: "architecture"
subagent_type: "general-purpose"
model: "<PAI_AGENT_MODEL 解析值(預設 opus,#20)>"
team_name: "ensemble-review-{timestamp}"
subagent_type: "general-purpose"
model: "<PAI_AGENT_MODEL 解析值(預設 opus,#20)>"
prompt: |
你是 Architecture Reviewer。
審閱範圍:{FILE_OR_DIR}
Expand All @@ -206,8 +211,10 @@ Agent:
Agent:
name: "correctness"
subagent_type: "general-purpose"
model: "<PAI_AGENT_MODEL 解析值(預設 opus,#20)>"
team_name: "ensemble-review-{timestamp}"
subagent_type: "general-purpose"
model: "<PAI_AGENT_MODEL 解析值(預設 opus,#20)>"
prompt: |
你是 Correctness Reviewer。
審閱範圍:{FILE_OR_DIR}
Expand All @@ -232,8 +239,10 @@ Agent:
Agent:
name: "security"
subagent_type: "general-purpose"
model: "<PAI_AGENT_MODEL 解析值(預設 opus,#20)>"
team_name: "ensemble-review-{timestamp}"
subagent_type: "general-purpose"
model: "<PAI_AGENT_MODEL 解析值(預設 opus,#20)>"
prompt: |
你是 Security Reviewer,以攻擊者視角審閱。
審閱範圍:{FILE_OR_DIR}
Expand All @@ -258,8 +267,10 @@ Agent:
Agent:
name: "devils-advocate"
subagent_type: "general-purpose"
model: "<PAI_AGENT_MODEL 解析值(預設 opus,#20)>"
team_name: "ensemble-review-{timestamp}"
subagent_type: "general-purpose"
model: "<PAI_AGENT_MODEL 解析值(預設 opus,#20)>"
prompt: |
你是 Devil's Advocate。
審閱範圍:{FILE_OR_DIR}
Expand Down
5 changes: 3 additions & 2 deletions plugins/parallel-ai-agents/skills/ensemble-compose/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,12 @@ FILE_OR_DIR — 審閱對象(缺則問使用者)
- `--lens 'perf: 檢查每個迴圈的時間複雜度...'` → `customLenses: [{key:"perf", focus:"檢查每個迴圈的時間複雜度..."}]`
- `--lens-file pack.csv` → **用 python3 csv 模組解析**(見 § CSV lens 包;focus 含逗號/中文標點,**不可** naive split)→ 每列轉 `{key, focus, needsSrt?}` append 到 `customLenses`(與 `--lens` 合併;同 key first-wins)
- `--replicas` / `--max-agents` / `--codex` → `replicas` / `maxAgents` / `codexEnabled`
3. 呼叫 `Workflow` tool,傳 `scriptPath` + `args`:
3. 解析 dispatch model(#20):`PAI_AGENT_MODEL` 未設 → `opus`;設了但不在 `sonnet|opus|haiku|fable` → **abort with usage error**(fail-loud,不靜默換模型;engine 對顯式非法值亦會於派發前 throw 作第二層)。解析值經 `args.agentModel` 傳入。接著呼叫 `Workflow` tool,傳 `scriptPath` + `args`:

```json
{
"profile": "custom",
"agentModel": "<PAI_AGENT_MODEL 解析值(預設 opus,#20)>",
"file": "<FILE_OR_DIR 絕對路徑>",
"contextBlock": "<檔案類型 emphasis + focus>",
"includeLenses": ["code.security", "academic.methodology"],
Expand All @@ -134,7 +135,7 @@ FILE_OR_DIR — 審閱對象(缺則問使用者)

## Legacy fallback(無 Workflow tool)

`Workflow` 不可用時,用 TeamCreate 把組裝後的 lens 各開一個 `general-purpose` Agent(role prompt = lens focus)、一個 devil's-advocate、可選 Codex Bash,同其他 skill 的 legacy 流程。所有 tool calls 同一 message 送出。
`Workflow` 不可用時,用 TeamCreate 把組裝後的 lens 各開一個 `general-purpose` Agent(role prompt = lens focus)、一個 devil's-advocate、可選 Codex Bash,同其他 skill 的 legacy 流程。所有 tool calls 同一 message 送出。每個 spawn 的 Agent 都帶顯式 `model: $PAI_AGENT_MODEL`(預設 `opus`,#20——不繼承 session 主迴圈模型)。

## 鐵律

Expand Down
3 changes: 2 additions & 1 deletion plugins/parallel-ai-agents/skills/ensemble-eval/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,12 @@ argument-hint: "[--fixture stats-paper] [--runs K] [--min-hits N] [--codex] [--a
### Phase 1: 跑 K 次真 ensemble

1. 解析 `FIX=${CLAUDE_PLUGIN_ROOT}/eval/fixtures/<fixture>`;`mktemp -d` 建 results 目錄。
2. **K 次**呼叫 `Workflow` tool(`scriptPath` = `${CLAUDE_PLUGIN_ROOT}/workflows/ensemble-workflow.js`):
2. 解析 dispatch model(#20):`PAI_AGENT_MODEL` 未設 → `opus`;設了但不在 `sonnet|opus|haiku|fable` → **abort with usage error**(fail-loud,不靜默換模型;engine 對顯式非法值亦會於派發前 throw 作第二層)。解析值經 `args.agentModel` 傳入。然後 **K 次**呼叫 `Workflow` tool(`scriptPath` = `${CLAUDE_PLUGIN_ROOT}/workflows/ensemble-workflow.js`):

```json
{
"profile": "academic",
"agentModel": "<PAI_AGENT_MODEL 解析值(預設 opus,#20)>",
"file": "<FIX>/paper.md",
"contextBlock": "學術論文審閱。ground-truth 計算 artifact:<FIX>/analysis/results.csv(number-verifier 逐數字比對用)。",
"codexEnabled": false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,12 @@ Arguments:
#### Backend A — Workflow(預設)

1. 解析 harness 絕對路徑:`${CLAUDE_PLUGIN_ROOT}/workflows/ensemble-workflow.js`。
2. 呼叫 `Workflow` tool,傳 `scriptPath`(上述絕對路徑)+ `args`:
2. 解析 dispatch model(#20):`PAI_AGENT_MODEL` 未設 → `opus`;設了但不在 `sonnet|opus|haiku|fable` → **abort with usage error**(fail-loud,不靜默換模型;engine 對顯式非法值亦會於派發前 throw 作第二層)。解析值經 `args.agentModel` 傳入。接著呼叫 `Workflow` tool,傳 `scriptPath`(上述絕對路徑)+ `args`:

```json
{
"profile": "lecture",
"agentModel": "<PAI_AGENT_MODEL 解析值(預設 opus,#20)>",
"file": "<講義 HTML 絕對路徑>",
"srtFile": "<SRT 絕對路徑或 null>",
"contextBlock": "<student_info + teaching context 組成的字串>",
Expand All @@ -78,9 +79,11 @@ Arguments:

#### Backend B — Legacy TeamCreate fan-out(fallback)

> 每個 spawn 的 Agent 都帶顯式 `model: $PAI_AGENT_MODEL`(預設 `opus`,#20——不繼承 session 主迴圈模型)。

**CRITICAL: 所有 4 個 Agent tool calls 必須在同一個 message 送出。**

**CRITICAL: Teammates 必須用 `subagent_type: "general-purpose"`。**
**CRITICAL: Teammates 必須用 `subagent_type: "general-purpose"`,且每個 Agent call 帶 `model: "<PAI_AGENT_MODEL 解析值(預設 opus,#20)>"`。**

用 TeamCreate 建立 team,然後在同一個 message 啟動 4 個 Agent:

Expand Down
42 changes: 42 additions & 0 deletions plugins/parallel-ai-agents/test/ensemble-workflow.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,48 @@ const boom = (label) => async (_p, o) => { if (o && o.label === label) throw new
const tests = []
const test = (name, fn) => tests.push({ name, fn })

test('dispatch model defaults to opus at EVERY agent() site (#20)', async () => {
const models = []
const capture = async (_p, o) => { models.push(o && o.model); return { findings: [] } }
const out = await runEnsemble({ profile: 'code', file: '/x', codexEnabled: true, codexCallPath: '/bin/cc' }, capture)
assert.ok(models.length >= 4, `expected reviewers+codex+da dispatches, got ${models.length}`)
assert.ok(models.every((m) => m === 'opus'), `non-opus dispatch found: ${JSON.stringify(models)}`)
assert.equal(out.stats.dispatchModel, 'opus')
})

test('agentModel override is honored at every site (#20)', async () => {
const models = []
const capture = async (_p, o) => { models.push(o && o.model); return { findings: [] } }
const out = await runEnsemble({ profile: 'code', file: '/x', codexEnabled: false, agentModel: 'sonnet' }, capture)
assert.ok(models.every((m) => m === 'sonnet'), JSON.stringify(models))
assert.equal(out.stats.dispatchModel, 'sonnet')
})

test('agentModel null / empty string ≡ absent → default opus, no throw (#20)', async () => {
for (const v of [null, '']) {
const models = []
const capture = async (_p, o) => { models.push(o && o.model); return { findings: [] } }
const out = await runEnsemble({ profile: 'code', file: '/x', codexEnabled: false, agentModel: v }, capture)
assert.ok(models.every((m) => m === 'opus'), `agentModel=${JSON.stringify(v)} → ${JSON.stringify(models)}`)
assert.equal(out.stats.dispatchModel, 'opus')
}
})

test('early-return guards carry dispatchModel in stats (#20 verify fix)', async () => {
const out = await runEnsemble({ profile: 'nope', file: '/x', agentModel: 'sonnet' }, allPass)
assert.equal(out.stats.dispatchModel, 'sonnet')
})

test('explicitly invalid agentModel throws BEFORE any dispatch (#20)', async () => {
let dispatched = 0
const counting = async () => { dispatched++; return { findings: [] } }
await assert.rejects(
() => runEnsemble({ profile: 'code', file: '/x', codexEnabled: false, agentModel: 'gpt-4' }, counting),
/invalid agentModel "gpt-4"/
)
assert.equal(dispatched, 0, 'no agent may be dispatched on an invalid model')
})

test('unknown profile → HIGH harness finding, agents=0', async () => {
const out = await runEnsemble({ profile: 'nope', file: '/x' }, allPass)
assert.equal(out.verdict, 'FINDINGS')
Expand Down
Loading
Loading