diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 2724e25..f75f2b8 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,8 @@ "homepage": "https://github.com/PsychQuant/issue-driven-development", "allowCrossMarketplaceDependenciesOn": [ "claude-plugins-official", - "parallel-ai-agents" + "parallel-ai-agents", + "codex-pro" ], "plugins": [ { diff --git a/openspec/changes/codex-channel-dependency/.openspec.yaml b/openspec/changes/codex-channel-dependency/.openspec.yaml new file mode 100644 index 0000000..2c990ac --- /dev/null +++ b/openspec/changes/codex-channel-dependency/.openspec.yaml @@ -0,0 +1,3 @@ +schema: spec-driven +created: 2026-07-18 +created_by: che cheng diff --git a/openspec/changes/codex-channel-dependency/design.md b/openspec/changes/codex-channel-dependency/design.md new file mode 100644 index 0000000..a57df1e --- /dev/null +++ b/openspec/changes/codex-channel-dependency/design.md @@ -0,0 +1,45 @@ +# Design: codex-channel-dependency + +## D1 — executable 歸 pai,前置區先解析(供兩用途) + +現行 SKILL 的共通前置在 Tier 1 之前算 `CODEX_CALL=$CLAUDE_PLUGIN_ROOT/bin/codex-call`、Tier 1 才算 `PAI_DIR`。重排:**前置區先解析 `PAI_DIR`**(同一段 `sort -V` cache glob),再取 `PAI_CODEX_CALL="$PAI_DIR/bin/codex-call"` — 一次解析、engine 路徑與 codex-call 路徑兩用。`MIN_PAI="2.19.0"`(gate 理由更新:2.18.0 引擎會**靜默忽略** codexModel/codexEffort → canonical tier 治理斷鏈,寧 fail 不降級)。pai 缺席/過舊 → 既有 Tier 2 fail-fast 訊息路徑;此時 codex leg 無 executable → 沿用既有 fail-closed INFO finding(cross-model pass incomplete),不 abort 整個 verify。 + +## D2 — 治理歸 codex-pro:contract-pinned,非 value-pinned + +```bash +CP_DIR=$(ls -d ~/.claude/plugins/cache/codex-pro/codex-pro/*/ 2>/dev/null | grep -E '/[0-9]+\.[0-9]+\.[0-9]+/$' | sort -V | tail -1) +MIN_CODEX_PRO="0.7.0" # defaults.json + profile-contract.md 起點 +# gate 失敗 → abort:claude plugin install codex-pro@codex-pro(superpowers 同款 fail-fast) +# 解析(per codex-pro references/profile-contract.md §2): +# defaults.json 起底(model/effort/max_time)→ ~/.codex-pro/profile.yaml → ./.codex-pro/profile.yaml(per-field 高層蓋低層) +``` + +IDD **pin 契約不 pin 值**(contract §4):樹內不出現任何 model 字面。defaults.json 缺檔/parse 失敗 → **fail-fast**(#205 fail-loud 判準 — 靜默跑錯 model 比失敗糟)。profile.yaml 解析沿 contract 的扁平 YAML 假設(python3 單行,同 codex-pro 慣例、不依賴 PyYAML)。 + +## D3 — 依賴接線:superpowers 形狀逐項對齊 + +| #209 superpowers | 本案 codex-pro | +|---|---| +| `dependencies` @ claude-plugins-official | `dependencies` @ codex-pro(self-hosted marketplace)| +| root marketplace `allowCrossMarketplaceDependenciesOn` | 同 — 加 `codex-pro` | +| `check-plugin-presence.sh` 三參數 pre-flight | `check-plugin-presence.sh codex-pro codex-pro` 於 idd-verify Step 0.5 前 | +| 缺席 fail-fast + 一步安裝指令 | 同(無 soft fallback — 使用者裁決「要 hard,保證品質」)| + +## D4 — canonical tier 傳參 + +Workflow args 增 `codexModel: $CODEX_MODEL, codexEffort: $CODEX_EFFORT`(pai 2.19.0 additive 契約)。`codexCallPath` 改傳 `$PAI_CODEX_CALL`。manual fan-out 的背景 codex 呼叫與 legacy 直呼段同步帶 `--model "$CODEX_MODEL" --effort "$CODEX_EFFORT" --max-time "$CODEX_MAX_TIME"`。 + +## D5 — drift-guard 重塑(single-pin 契約遷移) + +`model-generation-sync` 的不變量從「pin 在 IDD codex-call」改為「**IDD 樹內零 pin,契約指向 codex-pro**」: + +- assert `bin/codex-call` **不存在**(回歸 = 有人重新 vendor) +- assert SKILL 含 `MIN_CODEX_PRO="0.7.0"`、`defaults.json`、`codexModel`、`PAI_CODEX_CALL`、`MIN_PAI="2.19.0"` +- refute `--model gpt-5` / `gpt-5.5` hard-pin(維持);世代中立 prose(`gpt-5.x`)維持 +- idd-route / references 斷言維持不動 + +## Alternatives considered + +- **依賴 codex-pro 也提供 executable**:否 — codex-pro design D3 明文 executable 歸 pai;跨 plugin 職責不重排 +- **PATH 解析 `command -v codex-call`**(codex-pro setup 的做法):否 — IDD 已有 PAI_DIR 精確解析,PATH 受安裝順序影響(本案動機之一就是 PATH 上撞到 IDD 自己的 vendored copy) +- **保留 vendored copy 當 fallback**:否 — 使用者裁決 superpowers 形狀;divergence audit 證明 fork 必然靜默落後 diff --git a/openspec/changes/codex-channel-dependency/proposal.md b/openspec/changes/codex-channel-dependency/proposal.md new file mode 100644 index 0000000..36caac9 --- /dev/null +++ b/openspec/changes/codex-channel-dependency/proposal.md @@ -0,0 +1,29 @@ +# Proposal: codex-channel-dependency + +## Why + +IDD vendor 一份 `bin/codex-call`(#147 時代 — 當時 pai 還不是依賴)。前提已被 #207/#219 推翻(pai 是 install-time hard dependency、vendored ensemble fork 已刪,codex-call 卻留下)。實證兩則:divergence audit(#264 comment)顯示 vendored copy 落後 pai 2.18.0 四個安全/正確性修正(exp NSNumber parse、umask 0o077、form-encoding、flock re-read);#251 在 vendored copy 重做了 codex-pro#3 五天前已做過的 model bump。使用者裁決(2026-07-18):「完全依賴,跟 superpowers 一樣」。 + +## What Changes + +1. **刪除** `plugins/issue-driven-dev/bin/codex-call`(不留 fork — superpowers 形狀,#209 exemplar) +2. **executable 歸 pai**:codex-call 一律解析 `$PAI_DIR/bin/codex-call`(沿用既有 cache `sort -V` 解析);`MIN_PAI` 2.18.0 → **2.19.0**(codexModel/codexEffort args 起點,pai#22) +3. **治理歸 codex-pro**:新 governance 解析 — `MIN_CODEX_PRO=0.7.0` gate、讀 `references/defaults.json` 起底、疊 profile.yaml 兩層(per codex-pro `profile-contract.md` §2);解析失敗 fail-fast(不靜默 fallback) +4. **canonical tier 傳參**:Workflow args 加 `codexModel`/`codexEffort`(pai 2.19.0 契約);manual fan-out 與 SKILL 直呼處帶顯式 `--model`/`--effort` +5. **依賴接線**:`plugin.json` dependencies 加 `codex-pro@codex-pro`;root marketplace `allowCrossMarketplaceDependenciesOn` 加 `codex-pro`;Step 0.5 前 pre-flight `check-plugin-presence.sh codex-pro codex-pro`(缺席 fail-fast + 一步安裝指令) +6. **drift-guard 重塑**:`model-generation-sync` 的 single-pin 契約改指 codex-pro `defaults.json` — IDD 樹內零 model pin;`bin/codex-call` 不存在成為 assertion + +## Non-Goals + +- pai `bin/codex-call` 自身 default 的 bump(顯式傳參後無關;pai 另案) +- codex-pro producer skills 的 resolver 讀檔化(codex-pro#7 Residue) +- IDD_AGENT_MODEL(Claude dispatch model)機制 — 與 codex 治理正交,不動 + +## Impact + +- Affected specs: `idd-verify`(ADDED requirement) +- Affected code: `skills/idd-verify/SKILL.md`、刪 `bin/codex-call`、`scripts/tests/model-generation-sync/test.sh`、`.claude-plugin/plugin.json`、root `marketplace.json` + +## Refs + +Issue #264(diagnosis + divergence audit);上游 codex-pro#7(0.7.0,已結案)、pai#22(2.19.0,已結案) diff --git a/openspec/changes/codex-channel-dependency/specs/idd-verify/spec.md b/openspec/changes/codex-channel-dependency/specs/idd-verify/spec.md new file mode 100644 index 0000000..a4f2db2 --- /dev/null +++ b/openspec/changes/codex-channel-dependency/specs/idd-verify/spec.md @@ -0,0 +1,27 @@ +## ADDED Requirements + +### Requirement: Codex channel is fully dependency-resolved + +`idd-verify`'s codex channel SHALL contain no vendored executable and no model pin: the `codex-call` executable SHALL resolve from the installed `parallel-ai-agents` plugin cache (semver `sort -V`, gated `MIN_PAI` ≥ 2.19.0 — the `codexModel`/`codexEffort` contract floor), and model / effort / max-time governance SHALL resolve per `codex-pro`'s profile contract (gated `MIN_CODEX_PRO` ≥ 0.7.0): `references/defaults.json` as the base layer overlaid by the global then project `profile.yaml`. Resolved values SHALL be passed explicitly — as `codexModel` / `codexEffort` Workflow args on the canonical tier and as explicit `--model` / `--effort` flags on manual-fan-out invocations. A missing or too-old `codex-pro` installation, or an unreadable `defaults.json`, SHALL fail fast with a one-step install instruction and SHALL NOT silently fall back to any hardcoded model. + +#### Scenario: no vendored executable in the tree + +- **WHEN** the plugin tree is inspected +- **THEN** `plugins/issue-driven-dev/bin/codex-call` does not exist, and the SKILL resolves the executable from the pai plugin cache + +#### Scenario: governance flows from codex-pro to the canonical tier + +- **GIVEN** codex-pro 0.7.0+ installed with `defaults.json` model `gpt-5.6-sol` and no profile.yaml overrides +- **WHEN** the pai-ensemble canonical tier dispatches +- **THEN** the Workflow args include `codexModel: "gpt-5.6-sol"` and the codex leg runs that model + +#### Scenario: project profile overrides the default + +- **GIVEN** `./.codex-pro/profile.yaml` sets `effort: high` +- **WHEN** governance resolves +- **THEN** effort is `high` (project layer) while model still comes from `defaults.json` + +#### Scenario: absent codex-pro fails fast + +- **WHEN** codex-pro is not installed (or < 0.7.0) +- **THEN** idd-verify aborts before dispatch with the instruction `claude plugin install codex-pro@codex-pro`, and no ensemble runs on a guessed model diff --git a/openspec/changes/codex-channel-dependency/tasks.md b/openspec/changes/codex-channel-dependency/tasks.md new file mode 100644 index 0000000..d556fe6 --- /dev/null +++ b/openspec/changes/codex-channel-dependency/tasks.md @@ -0,0 +1,9 @@ +## 1. Tests first (RED) + +- [x] 1.1 (Req: Codex channel is fully dependency-resolved) Reshape scripts/tests/model-generation-sync/test.sh: single-pin 契約遷移 — assert `bin/codex-call` 不存在、SKILL 含 `MIN_CODEX_PRO="0.7.0"` / `defaults.json` / `codexModel` / `PAI_CODEX_CALL` / `MIN_PAI="2.19.0"` / 安裝指令 `claude plugin install codex-pro@codex-pro`;refute `--model gpt-5` hard-pin 維持;idd-route 斷言維持。Run: RED。 + +## 2. Implementation (GREEN) + +- [x] 2.1 (Design D1 — executable 歸 pai,前置區先解析; D2 — 治理歸 codex-pro:contract-pinned; D4 — canonical tier 傳參) skills/idd-verify/SKILL.md: 前置區重排(PAI_DIR 先解析 + PAI_CODEX_CALL + codex-pro governance 解析 block + fail-fast);MIN_PAI 2.19.0 + gate 理由;Workflow args 加 codexModel/codexEffort、codexCallPath 改 PAI_CODEX_CALL;manual fan-out 與 legacy 直呼段帶顯式 --model/--effort/--max-time;Step 0 TaskCreate 清單補 governance pre-flight。 +- [x] 2.2 (Design D3 — 依賴接線 superpowers 形狀) 刪 plugins/issue-driven-dev/bin/codex-call;plugin.json dependencies 加 codex-pro@codex-pro;root marketplace.json allowCrossMarketplaceDependenciesOn 加 codex-pro。 +- [x] 2.3 (Design D5 — drift-guard 重塑) Verify: model-generation-sync GREEN;全 plugin sweep 0 fail;spectra validate clean。 diff --git a/plugins/issue-driven-dev/.claude-plugin/plugin.json b/plugins/issue-driven-dev/.claude-plugin/plugin.json index 7d1cc0c..109e127 100644 --- a/plugins/issue-driven-dev/.claude-plugin/plugin.json +++ b/plugins/issue-driven-dev/.claude-plugin/plugin.json @@ -23,6 +23,10 @@ { "name": "parallel-ai-agents", "marketplace": "parallel-ai-agents" + }, + { + "name": "codex-pro", + "marketplace": "codex-pro" } ] } diff --git a/plugins/issue-driven-dev/bin/codex-call b/plugins/issue-driven-dev/bin/codex-call deleted file mode 100755 index 8634ebc..0000000 --- a/plugins/issue-driven-dev/bin/codex-call +++ /dev/null @@ -1,396 +0,0 @@ -#!/usr/bin/env swift - -// VENDORED from parallel-ai-agents@2.9.0 (bin/codex-call) — PsychQuant/issue-driven-development#147. -// Interpreted Swift script: no build / no notarize, just needs `swift` on PATH. -// This is a copy, NOT a symlink — idd-verify must work whether or not -// parallel-ai-agents is installed (zero runtime cross-plugin coupling). On an -// upstream fix, re-sync manually: -// cp ~/.claude/plugins/cache/parallel-ai-agents/parallel-ai-agents//bin/codex-call \ -// plugins/issue-driven-dev/bin/codex-call (then re-add this header) -// OAuth (~/.codex/auth.json) + refresh lock (~/.codex/.token-refresh.lock) are at -// fixed paths, so they stay shared across the vendored copy and the upstream one. - -// codex-call — direct HTTP wrapper for chatgpt.com/backend-api -// -// Replaces `codex exec --full-auto -o output "prompt"` with a clean HTTP call -// that bypasses the codex CLI subprocess (which can hang on stdin/stdout pipes). -// -// Usage: -// codex-call --output FILE [--model gpt-5.6-sol] [--effort xhigh] -// [--service-tier ""] [--max-time 600] -// [--instructions TEXT] [--prompt-file FILE | PROMPT] -// -// Reads OAuth token from ~/.codex/auth.json (codex CLI's token store). -// Auto-refreshes access_token if within 5 min of expiry. Refresh uses a file -// lock to prevent concurrent races during parallel ensemble runs. - -import Foundation -#if canImport(Darwin) -import Darwin -#endif - -// MARK: - Configuration - -let HOME_DIR = FileManager.default.homeDirectoryForCurrentUser -let AUTH_FILE = HOME_DIR.appendingPathComponent(".codex/auth.json").path -let LOCK_FILE = HOME_DIR.appendingPathComponent(".codex/.token-refresh.lock").path -let TOKEN_URL = URL(string: "https://auth.openai.com/oauth/token")! -let CODEX_URL = URL(string: "https://chatgpt.com/backend-api/codex/responses")! -let CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann" -let REFRESH_THRESHOLD_SEC: Int = 300 - -// MARK: - Helpers - -func die(_ msg: String, code: Int32 = 1) -> Never { - FileHandle.standardError.write(Data("error: \(msg)\n".utf8)) - exit(code) -} - -func log(_ msg: String) { - FileHandle.standardError.write(Data("[codex-call] \(msg)\n".utf8)) -} - -// MARK: - JWT exp - -func jwtExp(_ token: String) -> Int { - let parts = token.split(separator: ".") - guard parts.count >= 2 else { return 0 } - var payload = String(parts[1]) - .replacingOccurrences(of: "-", with: "+") - .replacingOccurrences(of: "_", with: "/") - while payload.count % 4 != 0 { payload += "=" } - guard let data = Data(base64Encoded: payload), - let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], - let exp = json["exp"] as? Int - else { return 0 } - return exp -} - -// MARK: - Auth file - -func loadAuthRaw() throws -> [String: Any] { - let data = try Data(contentsOf: URL(fileURLWithPath: AUTH_FILE)) - guard let obj = try JSONSerialization.jsonObject(with: data) as? [String: Any] - else { throw NSError(domain: "codex-call", code: 1, - userInfo: [NSLocalizedDescriptionKey: "auth.json is not an object"]) } - return obj -} - -func saveAuthRaw(_ auth: [String: Any]) throws { - let data = try JSONSerialization.data(withJSONObject: auth, options: [.prettyPrinted, .sortedKeys]) - let tmp = AUTH_FILE + ".tmp" - try data.write(to: URL(fileURLWithPath: tmp), options: .atomic) - _ = chmod(tmp, 0o600) - if rename(tmp, AUTH_FILE) != 0 { - throw NSError(domain: "codex-call", code: 2, - userInfo: [NSLocalizedDescriptionKey: "rename failed: \(String(cString: strerror(errno)))"]) - } -} - -// MARK: - OAuth refresh - -func httpFormPost(url: URL, fields: [String: String], timeout: TimeInterval = 30) throws -> Data { - var req = URLRequest(url: url, timeoutInterval: timeout) - req.httpMethod = "POST" - req.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") - let body = fields.map { (k, v) in - let ek = k.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? k - let ev = v.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? v - return "\(ek)=\(ev)" - }.joined(separator: "&") - req.httpBody = body.data(using: .utf8) - - let sem = DispatchSemaphore(value: 0) - var result: Data? - var error: Error? - var status: Int = 0 - URLSession.shared.dataTask(with: req) { data, resp, err in - status = (resp as? HTTPURLResponse)?.statusCode ?? 0 - result = data - error = err - sem.signal() - }.resume() - sem.wait() - - if let err = error { throw err } - guard status == 200, let data = result else { - let body = result.flatMap { String(data: $0, encoding: .utf8) } ?? "(no body)" - throw NSError(domain: "codex-call", code: status, - userInfo: [NSLocalizedDescriptionKey: "HTTP \(status): \(body.prefix(500))"]) - } - return data -} - -func refreshIfNeeded(_ auth: inout [String: Any]) throws { - guard var tokens = auth["tokens"] as? [String: Any], - let access = tokens["access_token"] as? String, - let refresh = tokens["refresh_token"] as? String - else { throw NSError(domain: "codex-call", code: 3, - userInfo: [NSLocalizedDescriptionKey: "auth.json missing tokens.access_token/refresh_token"]) } - - let now = Int(Date().timeIntervalSince1970) - if jwtExp(access) - now > REFRESH_THRESHOLD_SEC { return } - - // file lock - try FileManager.default.createDirectory( - at: URL(fileURLWithPath: LOCK_FILE).deletingLastPathComponent(), - withIntermediateDirectories: true) - let fd = open(LOCK_FILE, O_CREAT | O_WRONLY, 0o600) - if fd < 0 { - throw NSError(domain: "codex-call", code: 4, - userInfo: [NSLocalizedDescriptionKey: "open lock failed: \(String(cString: strerror(errno)))"]) - } - defer { close(fd) } - if flock(fd, LOCK_EX) != 0 { - throw NSError(domain: "codex-call", code: 5, - userInfo: [NSLocalizedDescriptionKey: "flock failed: \(String(cString: strerror(errno)))"]) - } - defer { _ = flock(fd, LOCK_UN) } - - // Re-read after acquiring lock — another process may have refreshed - auth = try loadAuthRaw() - if let t = auth["tokens"] as? [String: Any], - let a = t["access_token"] as? String, - jwtExp(a) - now > REFRESH_THRESHOLD_SEC { - return - } - tokens = auth["tokens"] as? [String: Any] ?? tokens - - let respData = try httpFormPost(url: TOKEN_URL, fields: [ - "grant_type": "refresh_token", - "refresh_token": refresh, - "client_id": CLIENT_ID, - ]) - guard let json = try JSONSerialization.jsonObject(with: respData) as? [String: Any], - let newAccess = json["access_token"] as? String - else { throw NSError(domain: "codex-call", code: 6, - userInfo: [NSLocalizedDescriptionKey: "refresh response missing access_token"]) } - - tokens["access_token"] = newAccess - if let newRefresh = json["refresh_token"] as? String { tokens["refresh_token"] = newRefresh } - if let newId = json["id_token"] as? String { tokens["id_token"] = newId } - auth["tokens"] = tokens - - let fmt = ISO8601DateFormatter() - fmt.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - auth["last_refresh"] = fmt.string(from: Date()) - - try saveAuthRaw(auth) - log("token refreshed") -} - -// MARK: - SSE streaming - -final class StreamCollector: NSObject, URLSessionDataDelegate { - var accumulated = "" - var buffer = "" - var statusCode: Int = 0 - var streamError: Error? - let done = DispatchSemaphore(value: 0) - var firstBytes: Data? - - func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, - didReceive response: URLResponse, - completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { - statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0 - completionHandler(.allow) - } - - func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { - if statusCode != 200 { - // Buffer error body for reporting - firstBytes = (firstBytes ?? Data()) + data - return - } - guard let chunk = String(data: data, encoding: .utf8) else { return } - buffer += chunk - while let range = buffer.range(of: "\n\n") { - let event = String(buffer[buffer.startIndex.. String? in - guard line.hasPrefix("data:") else { return nil } - return String(line.dropFirst(5)).trimmingCharacters(in: .whitespaces.subtracting(.newlines)) - } - let payload = dataLines.joined() - if payload.isEmpty || payload == "[DONE]" { return } - guard let data = payload.data(using: .utf8), - let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], - let type = json["type"] as? String - else { return } - - switch type { - case "response.output_text.delta": - if let delta = json["delta"] as? String { accumulated += delta } - case "error", "response.failed": - let msg = (json["message"] as? String) - ?? ((json["response"] as? [String: Any])?["error"] as? [String: Any])?["message"] as? String - ?? "Codex error" - streamError = NSError(domain: "codex-call", code: -1, - userInfo: [NSLocalizedDescriptionKey: msg]) - default: - break - } - } -} - -func streamCodex(prompt: String, outputFile: String, model: String, effort: String, - serviceTier: String, maxTime: Int, instructions: String) throws { - var auth = try loadAuthRaw() - try refreshIfNeeded(&auth) - guard let tokens = auth["tokens"] as? [String: Any], - let access = tokens["access_token"] as? String - else { throw NSError(domain: "codex-call", code: 7, - userInfo: [NSLocalizedDescriptionKey: "post-refresh: tokens missing"]) } - let accountId = (tokens["account_id"] as? String) ?? "" - - var body: [String: Any] = [ - "model": model, - "store": false, - "stream": true, - "instructions": instructions, - "input": [["role": "user", "content": [["type": "input_text", "text": prompt]]]], - "text": ["verbosity": "medium"], - "include": ["reasoning.encrypted_content"], - "tool_choice": "auto", - "parallel_tool_calls": true, - "reasoning": ["effort": effort, "summary": "auto"], - ] - // Translate legacy/friendly names to backend wire values. - // Mirrors codex-rs ServiceTier::request_value(): Fast→priority, Flex→flex. - // Backend rejects "fast" with HTTP 400; codex CLI does this translation internally. - let wireTier: String = { - switch serviceTier.lowercased() { - case "fast", "priority": return "priority" - case "flex": return "flex" - case "": return "" - default: return serviceTier // pass through unknown values - } - }() - if !wireTier.isEmpty { body["service_tier"] = wireTier } - - var req = URLRequest(url: CODEX_URL, timeoutInterval: TimeInterval(maxTime)) - req.httpMethod = "POST" - req.setValue("Bearer \(access)", forHTTPHeaderField: "Authorization") - req.setValue("application/json", forHTTPHeaderField: "Content-Type") - req.setValue("responses=experimental", forHTTPHeaderField: "OpenAI-Beta") - req.setValue("codex_cli_rs", forHTTPHeaderField: "originator") - req.setValue(accountId, forHTTPHeaderField: "chatgpt-account-id") - req.setValue("text/event-stream", forHTTPHeaderField: "Accept") - req.httpBody = try JSONSerialization.data(withJSONObject: body) - - let collector = StreamCollector() - let config = URLSessionConfiguration.default - config.timeoutIntervalForRequest = TimeInterval(maxTime) - config.timeoutIntervalForResource = TimeInterval(maxTime) - let session = URLSession(configuration: config, delegate: collector, delegateQueue: nil) - - session.dataTask(with: req).resume() - let waitResult = collector.done.wait(timeout: .now() + .seconds(maxTime + 5)) - session.invalidateAndCancel() - - if waitResult == .timedOut { - throw NSError(domain: "codex-call", code: 408, - userInfo: [NSLocalizedDescriptionKey: "Hard timeout after \(maxTime)s"]) - } - if let err = collector.streamError { throw err } - - try collector.accumulated.write(toFile: outputFile, atomically: true, encoding: .utf8) - log("wrote \(collector.accumulated.count) chars to \(outputFile)") -} - -// MARK: - Argument parsing - -struct Args { - var output: String? - var model: String = "gpt-5.6-sol" - var effort: String = "xhigh" - var serviceTier: String = "" - var maxTime: Int = 600 - var instructions: String = "You are a careful, rigorous reviewer. Respond in the user's language." - var promptFile: String? - var prompt: String? -} - -func parseArgs() -> Args { - var a = Args() - var args = Array(CommandLine.arguments.dropFirst()) - while !args.isEmpty { - let head = args.removeFirst() - func next() -> String { - guard !args.isEmpty else { die("missing value for \(head)") } - return args.removeFirst() - } - switch head { - case "--output", "-o": a.output = next() - case "--model": a.model = next() - case "--effort": a.effort = next() - case "--service-tier": a.serviceTier = next() - case "--max-time": a.maxTime = Int(next()) ?? a.maxTime - case "--instructions": a.instructions = next() - case "--prompt-file": a.promptFile = next() - case "--help", "-h": - print(""" - codex-call — direct HTTP wrapper for chatgpt.com/backend-api - - Usage: - codex-call --output FILE [--model gpt-5.6-sol] [--effort xhigh] - [--service-tier ""] [--max-time 600] - [--instructions TEXT] - [--prompt-file FILE | PROMPT] - - If no PROMPT and no --prompt-file is given, reads prompt from stdin. - - Auth: reads ~/.codex/auth.json (auto-refreshes if expired). - """) - exit(0) - default: - if a.prompt == nil { a.prompt = head } - else { die("unexpected positional argument: \(head)") } - } - } - return a -} - -// MARK: - Main - -let a = parseArgs() -guard let output = a.output else { die("--output is required") } - -let prompt: String -if let pf = a.promptFile { - do { prompt = try String(contentsOfFile: pf, encoding: .utf8) } - catch { die("cannot read prompt file: \(error.localizedDescription)") } -} else if let p = a.prompt { - prompt = p -} else { - let stdinData = FileHandle.standardInput.readDataToEndOfFile() - prompt = String(data: stdinData, encoding: .utf8) ?? "" -} -if prompt.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - die("empty prompt") -} - -do { - try streamCodex(prompt: prompt, outputFile: output, model: a.model, effort: a.effort, - serviceTier: a.serviceTier, maxTime: a.maxTime, instructions: a.instructions) -} catch { - die(error.localizedDescription) -} diff --git a/plugins/issue-driven-dev/scripts/tests/model-generation-sync/test.sh b/plugins/issue-driven-dev/scripts/tests/model-generation-sync/test.sh index 5eb93a7..0f0e550 100644 --- a/plugins/issue-driven-dev/scripts/tests/model-generation-sync/test.sh +++ b/plugins/issue-driven-dev/scripts/tests/model-generation-sync/test.sh @@ -1,13 +1,14 @@ #!/usr/bin/env bash -# test.sh — drift-guard for #251 model-generation sync. +# test.sh — drift-guard for the codex-channel dependency contract (#251 → #264). # -# CONTRACT: the codex model generation is pinned in EXACTLY ONE place — -# bin/codex-call's default. Every other site is generation-neutral: SKILL -# prose says gpt-5.x, codex invocations inherit the default (no explicit -# --model gpt-5.*), and the idd-route candidate is `codex-xhigh`. When the -# next generation ships, the bump touches codex-call + this suite's pin -# needle; any stale hard-pin elsewhere fails here instead of drifting silently -# (the #251 failure class: docs said gpt-5.5 long after the default moved). +# CONTRACT (v2 — #264 supersedes the #251 single-pin): IDD's tree contains NO +# vendored codex executable and NO model pin at all. The executable resolves +# from the parallel-ai-agents plugin cache (MIN_PAI ≥ 2.19.0, the +# codexModel/codexEffort contract floor); model/effort/max-time governance +# resolves from codex-pro's profile contract (MIN_CODEX_PRO ≥ 0.7.0, +# defaults.json + two profile.yaml layers) and is passed EXPLICITLY. A stale +# hard-pin, or a re-vendored bin/codex-call, fails here instead of drifting +# silently (the #251 failure class, now guarded one level up). # # Usage: bash test.sh (exit 0 = all pass, 1 = any fail) @@ -16,31 +17,47 @@ set -u HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PLUGIN_ROOT="$HERE/../../.." REPO_ROOT="$PLUGIN_ROOT/../.." -CODEX_CALL="$PLUGIN_ROOT/bin/codex-call" VERIFY="$PLUGIN_ROOT/skills/idd-verify/SKILL.md" DIAGNOSE="$PLUGIN_ROOT/skills/idd-diagnose/SKILL.md" ROUTE_RECOMMEND="$REPO_ROOT/plugins/idd-route/skills/idd-route-recommend/SKILL.md" ROUTE_STATS="$REPO_ROOT/plugins/idd-route/skills/idd-route-stats/SKILL.md" +PLUGIN_JSON="$PLUGIN_ROOT/.claude-plugin/plugin.json" HELPERS="$HERE/../../lib/assert-helpers.sh" [ -f "$HELPERS" ] || { echo "✗ missing $HELPERS — cannot run suite" >&2; exit 1; } . "$HELPERS" -assert_file_exists "codex-call exists" "$CODEX_CALL" assert_file_exists "idd-verify SKILL exists" "$VERIFY" assert_file_exists "idd-diagnose SKILL exists" "$DIAGNOSE" assert_file_exists "idd-route recommend SKILL exists" "$ROUTE_RECOMMEND" assert_file_exists "idd-route stats SKILL exists" "$ROUTE_STATS" -# ── single pin point: codex-call default (bump this needle with the next generation) ── -assert_output_grep "codex-call: default pinned to gpt-5.6-sol" 'var model: String = "gpt-5.6-sol"' "$CODEX_CALL" +# ── #264: no vendored executable — a re-vendor is a regression ── +if [ -e "$PLUGIN_ROOT/bin/codex-call" ]; then + fail "no vendored codex-call in the tree" "bin/codex-call exists — the #264 dependency contract deletes it (executable belongs to pai)" +else + pass "no vendored codex-call in the tree" +fi -# ── idd-verify: generation-neutral prose, invocations inherit the default ── +# ── executable resolution: pai, version-gated at the codexModel contract floor ── +assert_output_grep "skill: pai codex-call resolution var" 'PAI_CODEX_CALL' "$VERIFY" +assert_output_grep "skill: MIN_PAI gated at 2.19.0" 'MIN_PAI="2.19.0"' "$VERIFY" + +# ── governance resolution: codex-pro contract, fail-fast ── +assert_output_grep "skill: MIN_CODEX_PRO gate" 'MIN_CODEX_PRO="0.7.0"' "$VERIFY" +assert_output_grep "skill: reads codex-pro defaults.json" 'defaults.json' "$VERIFY" +assert_output_grep "skill: canonical tier passes codexModel" 'codexModel' "$VERIFY" +assert_output_grep "skill: one-step install instruction" 'claude plugin install codex-pro@codex-pro' "$VERIFY" + +# ── install-time dependency wiring ── +assert_output_grep "plugin.json: codex-pro dependency declared" '"codex-pro"' "$PLUGIN_JSON" + +# ── zero model pins in IDD's tree (generation-neutral prose only) ── refute_output_grep "idd-verify: no stale gpt-5.5 hard-pin" "gpt-5.5" "$VERIFY" assert_output_grep "idd-verify: prose is generation-neutral" "gpt-5.x" "$VERIFY" -refute_output_grep "idd-verify: codex invocation inherits default (no explicit --model gpt-5)" "--model gpt-5" "$VERIFY" +refute_output_grep "idd-verify: no hardcoded --model gpt-5 pin" '--model gpt-5' "$VERIFY" -# ── idd-diagnose: candidate renamed to generation-neutral ── +# ── idd-diagnose: candidate stays generation-neutral ── refute_output_grep "idd-diagnose: old candidate name gone" "codex-gpt-5.5-xhigh" "$DIAGNOSE" assert_output_grep "idd-diagnose: codex-xhigh candidate" "codex-xhigh" "$DIAGNOSE" diff --git a/plugins/issue-driven-dev/skills/idd-verify/SKILL.md b/plugins/issue-driven-dev/skills/idd-verify/SKILL.md index dcdd5c5..0e6e5f7 100644 --- a/plugins/issue-driven-dev/skills/idd-verify/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-verify/SKILL.md @@ -187,10 +187,27 @@ esac ``` # 共通前置(Tier 1/2 都需要) write $DIFF 到 temp 檔 $DIFF_FILE # 大 diff 不塞 inline args (Workflow tool 會把 args JSON-stringify;兩個引擎都防禦性 parse 並支援 diffFile 讓 agents 用 file-read tool 讀) -# #147: 在 skill-run context 把 $CLAUDE_PLUGIN_ROOT **解析成絕對路徑**再 thread(workflow subagent 的 shell 沒有這個變數)。 -# 一律用 IDD 自己 vendored 的 codex-call —— 不依賴 pai 的 bin 佈局(契約參數是 codexCallPath,路徑由 consumer 供給)。 -CODEX_CALL=$(realpath "$CLAUDE_PLUGIN_ROOT/bin/codex-call" 2>/dev/null || echo "$CLAUDE_PLUGIN_ROOT/bin/codex-call") -# (Tier 3 manual fan-out 不消費以上輸出——它自寫 /tmp/diff_$NUMBER.patch 並直呼 codex-call;落到 Tier 3 時本前置為無害冗餘) + +# ── codex 通道依賴解析(#264:完全依賴,superpowers 形狀 — vendored codex-call 已刪除)── +# executable 歸 pai(design D3 分工):PAI_DIR 前置先解析,engine 路徑與 codex-call 路徑兩用。 +PAI_DIR=$(ls -d ~/.claude/plugins/cache/parallel-ai-agents/parallel-ai-agents/*/ 2>/dev/null | grep -E '/[0-9]+\.[0-9]+\.[0-9]+/$' | sort -V | tail -1) # semver 目錄才參賽(防 latest/current 誤入版本比較) +PAI_VER=$(basename "$PAI_DIR" 2>/dev/null) +PAI_CODEX_CALL="${PAI_DIR}bin/codex-call" # 取代舊 vendored copy(divergence audit:vendored 版落後 pai 四個修正,#264) + +# 治理歸 codex-pro(EXTERNAL-CONSUMER CONTRACT:references/profile-contract.md + defaults.json)—— +# IDD pin 契約不 pin 值:樹內零 model 字面,換代只動 codex-pro 一處。 +MIN_CODEX_PRO="0.7.0" # defaults.json + profile-contract 起點(codex-pro#7) +CP_DIR=$(ls -d ~/.claude/plugins/cache/codex-pro/codex-pro/*/ 2>/dev/null | grep -E '/[0-9]+\.[0-9]+\.[0-9]+/$' | sort -V | tail -1) +CP_VER=$(basename "$CP_DIR" 2>/dev/null) +CP_DEFAULTS="${CP_DIR}references/defaults.json" +若 [ ! -f "$CP_DEFAULTS" ] 或 [ "$(printf '%s\n%s\n' "$MIN_CODEX_PRO" "$CP_VER" | sort -V | head -1)" != "$MIN_CODEX_PRO" ]: + abort(fail-fast,無 soft fallback —— 靜默跑錯 model 比失敗糟,#205 判準): + "✗ codex-pro governance unavailable(缺席 / $CP_VER < $MIN_CODEX_PRO / defaults.json 缺檔——三者印其一)" + " Install (one step): claude plugin install codex-pro@codex-pro" +# 解析(per contract §2:defaults.json 起底 → ~/.codex-pro/profile.yaml → ./.codex-pro/profile.yaml,per-field 高層蓋低層): +CODEX_MODEL / CODEX_EFFORT / CODEX_MAX_TIME ← python3 讀 $CP_DEFAULTS 起底,再逐層疊 profile.yaml 同名欄位(扁平 YAML regex parse,同 codex-pro 慣例、不依賴 PyYAML) +# (Tier 3 manual fan-out 消費 $PAI_CODEX_CALL 與治理三值;pai 連 cache 都缺席時 codex leg 無 executable → +# 沿用 fail-closed INFO finding「cross-model pass incomplete」,不 abort 整個 verify) # $CONTEXT_BLOCK 組裝(Tier 1 專用)——pai 契約把 issue context 收斂成單一字串;DATA_GUARD 前言是 IDD 端第一層 # injection 防護(pai 端 dataBlock() 會對整塊再包 PAI_ENSEMBLE sentinel 並剝除偽造 marker——雙層) @@ -208,9 +225,8 @@ CONTEXT_BLOCK="${CONTEXT_BLOCK} Source-of-truth attachments (repo-relative; read with your file tools): ${ATTACHMENT_LIST:-(none)}" # Tier 1 — canonical:已安裝的 parallel-ai-agents 引擎(#207 使用者依賴裁決;契約 = pai#20 官方化的 EXTERNAL-CONSUMER CONTRACT) -MIN_PAI="2.18.0" # agentModel + STABLE 契約起點——閘門理由:2.17.0 引擎會「靜默忽略」agentModel → 派發回退繼承 session model(#205 根因復發且揭露行造假),寧用 Tier 2 已修的 fork -PAI_DIR=$(ls -d ~/.claude/plugins/cache/parallel-ai-agents/parallel-ai-agents/*/ 2>/dev/null | grep -E '/[0-9]+\.[0-9]+\.[0-9]+/$' | sort -V | tail -1) # semver 目錄才參賽(防 latest/current 誤入版本比較) -PAI_VER=$(basename "$PAI_DIR" 2>/dev/null) +MIN_PAI="2.19.0" # codexModel/codexEffort 契約起點(pai#22)——閘門理由:2.18.0 引擎會「靜默忽略」這兩個 args → canonical tier 的 codex 治理斷鏈(#264;同 #205 的 agentModel 教訓:靜默忽略比失敗糟) +# PAI_DIR / PAI_VER 已於共通前置解析(#264 重排 —— codex-call 路徑與 engine 路徑兩用) PAI_ENGINE="${PAI_DIR}workflows/ensemble-workflow.js" 若 [ -f "$PAI_ENGINE" ] 且 [ "$(printf '%s\n%s\n' "$MIN_PAI" "$PAI_VER" | sort -V | head -1)" = "$MIN_PAI" ] 且 dynamic-workflow primitive 可用: # contextBlock:DATA_GUARD 前言(與 vendored 引擎同文)+ 各 issue "ISSUE #N: \n<body>" + "Source-of-truth attachments: <清單>" @@ -225,7 +241,8 @@ PAI_ENGINE="${PAI_DIR}workflows/ensemble-workflow.js" daFocus: "adversarially refute the other reviewers' judgments: hunt for defects where they passed, false positives in their findings, and requirements-coverage claims the diff does not actually satisfy.", contextBlock: $CONTEXT_BLOCK, diffFile: $DIFF_FILE, - codexEnabled, codexCallPath: $CODEX_CALL, + codexEnabled, codexCallPath: $PAI_CODEX_CALL, + codexModel: $CODEX_MODEL, codexEffort: $CODEX_EFFORT, # #264:codex-pro 治理值(pai 2.19.0 契約 args) agentModel: $AGENT_MODEL}) # = Step 2 前解析的 IDD_AGENT_MODEL 值(#205);pai 端顯式非法值派發前 throw BACKEND_DESC="pai-ensemble ${PAI_VER} (canonical #207) — 4 IDD lenses + DA + Codex (gpt-5.x)" 印一行 notice: "→ verify backend: pai-ensemble $PAI_VER (canonical, #207)" @@ -245,7 +262,7 @@ PAI_ENGINE="${PAI_DIR}workflows/ensemble-workflow.js" 三條路產出**相同 findings contract**(見 `references/idd-verify-findings-schema.json`:severity / file / title / body / lens;merge 取最高),所以 Step 3 merge 之後(posting / triage / verify-fix)**backend-agnostic**。 -**Codex(D3)**:包進 workflow 當 Bash agent,透過 vendored **`codex-call` HTTP wrapper**(#147,`$CLAUDE_PLUGIN_ROOT/bin/codex-call`,由 args thread 絕對路徑進來;pai canonical 契約欄位 = `codexCallPath`)—— 直打 chatgpt codex backend,**非** `codex exec` subprocess,故無 stdin/stdout pipe 互鎖 hang;`--max-time 600` 是硬 HTTP timeout(codex CLI 不一定守)。runtime 依賴從 `codex` CLI 換成 `swift` 在 PATH。codex-call 失敗(swift 缺 / HTTP 5xx / auth refresh / timeout)→ 回 fail-closed INFO finding「cross-model pass incomplete」不靜默丟(對應 spec「bounded lifetime」requirement);**刻意不 fallback `codex exec`**(會重引 hang 路徑)。 +**Codex(D3)**:包進 workflow 當 Bash agent,透過 **pai 的 `codex-call` HTTP wrapper**(#264 依賴化:`$PAI_CODEX_CALL`,由 args thread 絕對路徑進來;pai canonical 契約欄位 = `codexCallPath`,model/effort 由 `codexModel`/`codexEffort` args 傳入 codex-pro 治理值)—— 直打 chatgpt codex backend,**非** `codex exec` subprocess,故無 stdin/stdout pipe 互鎖 hang;`--max-time 600` 是硬 HTTP timeout(codex CLI 不一定守)。runtime 依賴從 `codex` CLI 換成 `swift` 在 PATH。codex-call 失敗(swift 缺 / HTTP 5xx / auth refresh / timeout)→ 回 fail-closed INFO finding「cross-model pass incomplete」不靜默丟(對應 spec「bounded lifetime」requirement);**刻意不 fallback `codex exec`**(會重引 hang 路徑)。 **Interaction 軸(D5)**:workflow 跑背景 = 本質 unattended(no mid-run input),對齊 `idd-pr-hitl-modes` 的 interaction 軸——verify core 內零 user input,所有 gates/triage/verify-fix 在 core 前/後(skill 端)。 @@ -271,7 +288,7 @@ TaskCreate(name="gate_pr_correspondence", description="Step 0.7: PR mode 下強 TaskCreate(name="scan_pr_body_and_commits_trailers", description="Step 0.8: PR mode 下兩 source 偵測 auto-close trap — (1) gh pr view --json closingIssuesReferences 查 PR body 是否 linked-to-auto-close(GitHub 權威解析、所有 trailer 形式),(2) gh pr view --json commits 對每個 commit messageBody 跑 trap regex(補上 GitHub 不預計算的 commit-body channel — squash 後字串 land 在 main 觸發 auto-close)。任一非空則 warn — bypass /idd-close gate。Warn-only,不 abort") TaskCreate(name="get_diff_and_issue", description="依 input source 取 diff(gh pr diff / git diff HEAD~N / git diff origin/<default>...<branch>) + gh issue view,存 diff 到 /tmp 供 agents 讀取,並記 FROZEN_SHA=$(git rev-parse HEAD)(PR mode 記 PR head oid — #228 freshness 錨點);PR mode 額外做 gh pr checkout 並記住原 branch") TaskCreate(name="check_attachments", description="確認 .claude/.idd/attachments/issue-NNN/ 存在,把 attachment 路徑塞進 reviewer agent prompt 作為 source-of-truth context。manifest 缺漏 → 警告繼續(reviewer 仍跑,但 verification 完整度受限)。依 rules/process-attachments.md。") -TaskCreate(name="resolve_dispatch_model", description="解析 $AGENT_MODEL — IDD_AGENT_MODEL 未設 → opus;非法值 → abort with usage error(#205;兩個 backend 共用,Workflow args 傳 agentModel、manual 模板填 model)") +TaskCreate(name="resolve_dispatch_model", description="解析 $AGENT_MODEL — IDD_AGENT_MODEL 未設 → opus;非法值 → abort with usage error(#205;兩個 backend 共用,Workflow args 傳 agentModel、manual 模板填 model);#264 同步解析 codex 治理(check-plugin-presence.sh codex-pro codex-pro → CP defaults.json + profile.yaml 兩層 → CODEX_MODEL/EFFORT/MAX_TIME,缺席 fail-fast)") TaskCreate(name="launch_parallel_reviewers", description="第一波 5 個 tool calls 同一 message: 4 lens Agent(subagent_type=general-purpose, model=$AGENT_MODEL) for requirements/logic/security/regression + 1 Bash codex(run_in_background:true);DA 不在此波(#130 sequenced)。prompt 引用 attachment 路徑 + 強制 file-output rule (per #52)") TaskCreate(name="spawn_sequenced_da", description="#130: 4 份 lens findings 檔全部就緒(non-empty)後,coordinator 序列 spawn Devil's Advocate(model=$AGENT_MODEL,prompt 直附 4 檔路徑,無 polling)") TaskCreate(name="wait_for_claude_agents", description="4 lens Agent calls return 後 ls /tmp/verify_${NUMBER}_findings_*.md 確認 4 檔 non-empty(DA 檔在 sequenced spawn 後另計);缺者進 Step 2.5 Recovery Protocol") @@ -653,11 +670,11 @@ If you receive a later SendMessage with the same prompt re-pasted, treat as retr #### 2b. Codex(背景執行,via vendored `codex-call` HTTP wrapper,#147) -透過 vendored `codex-call`(HTTP,非 `codex exec` subprocess → 無 pipe hang)執行 review。**注意語意差異**:`codex exec --full-auto` 是 agentic(codex 自己讀 working-tree diff);`codex-call` 是單次 completion(非 agentic),所以 diff 必須**顯式**用 `--prompt-file` 餵進去 —— 用 Step 1 已寫好的 `/tmp/diff_$NUMBER.patch`,review 框架放 `--instructions`: +透過 pai 的 `codex-call`(`$PAI_CODEX_CALL`;HTTP,非 `codex exec` subprocess → 無 pipe hang)執行 review,model/effort 帶共通前置解析的 codex-pro 治理值(#264)。**注意語意差異**:`codex exec --full-auto` 是 agentic(codex 自己讀 working-tree diff);`codex-call` 是單次 completion(非 agentic),所以 diff 必須**顯式**用 `--prompt-file` 餵進去 —— 用 Step 1 已寫好的 `/tmp/diff_$NUMBER.patch`,review 框架放 `--instructions`: ```bash Bash({ - command: `"$CLAUDE_PLUGIN_ROOT/bin/codex-call" --output /tmp/codex-verify-$NUMBER.md --effort xhigh --service-tier fast --max-time 600 --prompt-file /tmp/diff_$NUMBER.patch --instructions "You are verifying code changes for Issue #$NUMBER: $TITLE. Go through EACH requirement: FULLY / PARTIALLY / NOT addressed. Flag scope creep and regressions. Reply in Traditional Chinese."`, + command: `"$PAI_CODEX_CALL" --output /tmp/codex-verify-$NUMBER.md --model "$CODEX_MODEL" --effort "$CODEX_EFFORT" --service-tier fast --max-time "$CODEX_MAX_TIME" --prompt-file /tmp/diff_$NUMBER.patch --instructions "You are verifying code changes for Issue #$NUMBER: $TITLE. Go through EACH requirement: FULLY / PARTIALLY / NOT addressed. Flag scope creep and regressions. Reply in Traditional Chinese."`, description: "Codex review for #$NUMBER (via codex-call)", run_in_background: true }) @@ -1094,14 +1111,14 @@ Full integration contract: [`references/agent-routing.md`](../../references/agen ```bash # codex-call 是單次 completion(非 agentic)→ 顯式把 diff 餵進去 git diff > /tmp/codex-quick-diff.patch -"$CLAUDE_PLUGIN_ROOT/bin/codex-call" \ +"$PAI_CODEX_CALL" \ --output /tmp/codex-quick-review.md \ - --effort xhigh --service-tier fast --max-time 600 \ + --model "$CODEX_MODEL" --effort "$CODEX_EFFORT" --service-tier fast --max-time "$CODEX_MAX_TIME" \ --prompt-file /tmp/codex-quick-diff.patch \ --instructions "Review this git diff. Flag bugs, logic errors, security issues. Reply in Traditional Chinese." ``` -> **codex-call note(#147)**:透過 vendored `codex-call` HTTP wrapper(`$CLAUDE_PLUGIN_ROOT/bin/codex-call`)而非 `codex exec` subprocess —— 無 pipe hang、`--max-time` 硬 timeout、依賴 `swift` 在 PATH。`--service-tier fast` 加速 GPT-5.5 回應(需較多 credits,換取 2-5x 速度)。驗證場景對速度敏感(user 在等 findings),預設開啟;若要省 credit 可移除此 flag。 +> **codex-call note(#147 → #264 依賴化)**:透過 pai 的 `codex-call` HTTP wrapper(`$PAI_CODEX_CALL`,vendored copy 已刪)而非 `codex exec` subprocess —— 無 pipe hang、`--max-time` 硬 timeout、依賴 `swift` 在 PATH。`--service-tier fast` 加速 gpt-5.x 回應(需較多 credits,換取 2-5x 速度)。驗證場景對速度敏感(user 在等 findings),預設開啟;若要省 credit 可移除此 flag。 ## Engine: team(只用 5 Reviewer Agents,alias `team` 保留為 backward-compat)