Merge pull request #269 from nrslib/release/v0.13.0

Release v0.13.0
This commit is contained in:
nrs 2026-02-13 19:24:18 +09:00 committed by GitHub
commit 652630eeca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
76 changed files with 711 additions and 23 deletions

View File

@ -4,12 +4,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [0.13.0-alpha.1] - 2026-02-13 ## [0.13.0] - 2026-02-13
### Added ### Added
- **Team Leader ムーブメント**: ムーブメント内でチームリーダーエージェントがタスクを動的にサブタスクPartへ分解し、複数のパートエージェントを並列実行する新しいムーブメントタイプ — `team_leader` 設定persona, maxParts, timeoutMs, partPersona, partEdit, partPermissionModeをサポート (#244) - **Team Leader ムーブメント**: ムーブメント内でチームリーダーエージェントがタスクを動的にサブタスクPartへ分解し、複数のパートエージェントを並列実行する新しいムーブメントタイプ — `team_leader` 設定persona, maxParts, timeoutMs, partPersona, partEdit, partPermissionModeをサポート (#244)
- **構造化出力Structured Output**: エージェント呼び出しに JSON Schema ベースの構造化出力を導入 — タスク分解decomposition、ルール評価evaluation、ステータス判定judgmentの3つのスキーマを `builtins/schemas/` に追加。Claude / Codex 両プロバイダーで対応 (#257) - **構造化出力Structured Output**: エージェント呼び出しに JSON Schema ベースの構造化出力を導入 — タスク分解decomposition、ルール評価evaluation、ステータス判定judgmentの3つのスキーマを `builtins/schemas/` に追加。Claude / Codex 両プロバイダーで対応 (#257)
- **`provider_options` ピースレベル設定**: ピース全体(`piece_config.provider_options`)および個別ムーブメントにプロバイダー固有オプション(`codex.network_access``opencode.network_access`)を設定可能に — 全ビルトインピースに Codex/OpenCode のネットワークアクセスを有効化
- **`backend` ビルトインピース**: バックエンド開発特化のピースを新規追加 — バックエンド、セキュリティ、QA の並列専門家レビュー対応 - **`backend` ビルトインピース**: バックエンド開発特化のピースを新規追加 — バックエンド、セキュリティ、QA の並列専門家レビュー対応
- **`backend-cqrs` ビルトインピース**: CQRS+ES 特化のバックエンド開発ピースを新規追加 — CQRS+ES、セキュリティ、QA の並列専門家レビュー対応 - **`backend-cqrs` ビルトインピース**: CQRS+ES 特化のバックエンド開発ピースを新規追加 — CQRS+ES、セキュリティ、QA の並列専門家レビュー対応
- **AbortSignal によるパートタイムアウト**: Team Leader のパート実行にタイムアウト制御と親シグナル連動の AbortSignal を追加 - **AbortSignal によるパートタイムアウト**: Team Leader のパート実行にタイムアウト制御と親シグナル連動の AbortSignal を追加
@ -21,6 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- **Phase 3 判定ロジックの刷新**: `JudgmentDetector` / `FallbackStrategy` を廃止し、構造化出力ベースの `status-judgment-phase.ts` に統合。判定の安定性と保守性を向上 (#257) - **Phase 3 判定ロジックの刷新**: `JudgmentDetector` / `FallbackStrategy` を廃止し、構造化出力ベースの `status-judgment-phase.ts` に統合。判定の安定性と保守性を向上 (#257)
- **Report フェーズのリトライ改善**: Report PhasePhase 2が失敗した場合、新規セッションで自動リトライするよう改善 (#245) - **Report フェーズのリトライ改善**: Report PhasePhase 2が失敗した場合、新規セッションで自動リトライするよう改善 (#245)
- **Ctrl+C シャットダウンの統一**: `sigintHandler.ts` を廃止し、`ShutdownManager` に統合 — グレースフルシャットダウン → タイムアウト → 強制終了の3段階制御を全プロバイダーで共通化 (#237) - **Ctrl+C シャットダウンの統一**: `sigintHandler.ts` を廃止し、`ShutdownManager` に統合 — グレースフルシャットダウン → タイムアウト → 強制終了の3段階制御を全プロバイダーで共通化 (#237)
- **スコープ外削除の防止ガードレール**: coder ペルソナにタスク指示書の範囲外の削除・構造変更を禁止するルールを追加。planner ペルソナにスコープ規律と参照資料の優先順位を追加
- フロントエンドナレッジにデザイントークンとテーマスコープのガイダンスを追加 - フロントエンドナレッジにデザイントークンとテーマスコープのガイダンスを追加
- アーキテクチャナレッジの改善en/ja 両対応) - アーキテクチャナレッジの改善en/ja 両対応)
@ -39,6 +41,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- AbortSignal のユニットテスト追加abort-signal, claude-executor-abort-signal, claude-provider-abort-signal - AbortSignal のユニットテスト追加abort-signal, claude-executor-abort-signal, claude-provider-abort-signal
- Report Phase リトライのユニットテスト追加report-phase-retry - Report Phase リトライのユニットテスト追加report-phase-retry
- パブリック API エクスポートのユニットテスト追加public-api-exports - パブリック API エクスポートのユニットテスト追加public-api-exports
- provider_options 関連のテスト追加provider-options-piece-parser, models, opencode-types
- E2E テストの大幅拡充: cycle-detection, model-override, multi-step-sequential, pipeline-local-repo, report-file-output, run-sigint-graceful, session-log, structured-output, task-status-persistence - E2E テストの大幅拡充: cycle-detection, model-override, multi-step-sequential, pipeline-local-repo, report-file-output, run-sigint-graceful, session-log, structured-output, task-status-persistence
- E2E テストヘルパーのリファクタリング(共通 setup 関数の抽出) - E2E テストヘルパーのリファクタリング(共通 setup 関数の抽出)
- `judgment/` ディレクトリJudgmentDetector, FallbackStrategyを削除 - `judgment/` ディレクトリJudgmentDetector, FallbackStrategyを削除

View File

@ -803,10 +803,22 @@ Special `next` values: `COMPLETE` (success), `ABORT` (failure)
| `provider` | - | Override provider for this movement (`claude`, `codex`, or `opencode`) | | `provider` | - | Override provider for this movement (`claude`, `codex`, or `opencode`) |
| `model` | - | Override model for this movement | | `model` | - | Override model for this movement |
| `permission_mode` | - | Permission mode: `readonly`, `edit`, `full` (provider-independent) | | `permission_mode` | - | Permission mode: `readonly`, `edit`, `full` (provider-independent) |
| `provider_options` | - | Provider-specific options (e.g. `codex.network_access`, `opencode.network_access`) |
| `output_contracts` | - | Output contract definitions for report files | | `output_contracts` | - | Output contract definitions for report files |
| `quality_gates` | - | AI directives for movement completion requirements | | `quality_gates` | - | AI directives for movement completion requirements |
| `mcp_servers` | - | MCP (Model Context Protocol) server configuration (stdio/SSE/HTTP) | | `mcp_servers` | - | MCP (Model Context Protocol) server configuration (stdio/SSE/HTTP) |
Piece-level defaults can be set with `piece_config.provider_options`, and movement-level `provider_options` overrides them.
```yaml
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
```
## API Usage Example ## API Usage Example
```typescript ```typescript

View File

@ -35,3 +35,4 @@ You are the implementer. Focus on implementation, not design decisions.
- Adding backward compatibility or legacy support without being asked → Absolutely prohibited - Adding backward compatibility or legacy support without being asked → Absolutely prohibited
- Leaving replaced code/exports after refactoring → Prohibited (remove unless explicitly told to keep) - Leaving replaced code/exports after refactoring → Prohibited (remove unless explicitly told to keep)
- Layering workarounds that bypass safety mechanisms on top of a root cause fix → Prohibited - Layering workarounds that bypass safety mechanisms on top of a root cause fix → Prohibited
- Deleting existing features or structural changes not in the task order as a "side effect" → Prohibited (report even if included in the plan, when there's no basis in the task order for large-scale deletions)

View File

@ -50,6 +50,23 @@ Judge from a big-picture perspective to avoid "missing the forest for the trees.
| Non-functional Requirements | Are performance, security, etc. met? | | Non-functional Requirements | Are performance, security, etc. met? |
| Scope | Is there scope creep beyond requirements? | | Scope | Is there scope creep beyond requirements? |
### Scope Creep Detection (Deletions are Critical)
File **deletions** and removal of existing features are the most dangerous form of scope creep.
Additions can be reverted, but restoring deleted flows is difficult.
**Required steps:**
1. List all deleted files (D) and deleted classes/methods/endpoints from the diff
2. Cross-reference each deletion against the task order to find its justification
3. REJECT any deletion that has no basis in the task order
**Typical scope creep patterns:**
- A "change statuses" task includes wholesale deletion of Sagas or endpoints
- A "UI fix" task includes structural changes to backend domain models
- A "display change" task rewrites business logic flows
Even if reviewers approved a deletion as "sound design," REJECT it if it's outside the task order scope.
### 3. Risk Assessment ### 3. Risk Assessment
**Risk Matrix:** **Risk Matrix:**

View File

@ -86,11 +86,22 @@ Based on investigation and design, determine the implementation direction:
- Points to be careful about - Points to be careful about
- Spec constraints - Spec constraints
## Scope Discipline
Only plan work that is explicitly stated in the task order. Do not include implicit "improvements."
**Deletion criteria:**
- **Code made newly unused by this task's changes** → OK to plan deletion (e.g., renamed old variable)
- **Existing features, flows, endpoints, Sagas, events** → Do NOT delete unless explicitly instructed in the task order
"Change statuses to 5 values" means "rewrite enum values," NOT "delete flows that seem unnecessary."
Do not over-interpret the task order. Plan only what is written.
## Design Principles ## Design Principles
**Backward Compatibility:** **Backward Compatibility:**
- Do not include backward compatibility code unless explicitly instructed - Do not include backward compatibility code unless explicitly instructed
- Plan to delete things that are unused - Delete code that was made newly unused by this task's changes
**Don't Generate Unnecessary Code:** **Don't Generate Unnecessary Code:**
- Don't plan "just in case" code, future fields, or unused methods - Don't plan "just in case" code, future fields, or unused methods

View File

@ -100,6 +100,21 @@ Check:
**REJECT if spec violations are found.** Don't assume "probably correct"—actually read and cross-reference the specs. **REJECT if spec violations are found.** Don't assume "probably correct"—actually read and cross-reference the specs.
### Scope Creep Detection (Deletions are Critical)
File **deletions** and removal of existing features are the most dangerous form of scope creep.
Additions can be reverted, but restoring deleted flows is difficult.
**Required steps:**
1. List all deleted files (D) and deleted classes/methods/endpoints from the diff
2. Cross-reference each deletion against the task order to find its justification
3. REJECT any deletion that has no basis in the task order
**Typical scope creep patterns:**
- A "change statuses" task includes wholesale deletion of Sagas or endpoints
- A "UI fix" task includes structural changes to backend domain models
- A "display change" task rewrites business logic flows
### 8. Piece Overall Review ### 8. Piece Overall Review
**Check all reports in the report directory and verify overall piece consistency.** **Check all reports in the report directory and verify overall piece consistency.**
@ -115,7 +130,7 @@ Check:
| Plan-implementation gap | REJECT - Request plan revision or implementation fix | | Plan-implementation gap | REJECT - Request plan revision or implementation fix |
| Unaddressed review feedback | REJECT - Point out specific unaddressed items | | Unaddressed review feedback | REJECT - Point out specific unaddressed items |
| Deviation from original purpose | REJECT - Request return to objective | | Deviation from original purpose | REJECT - Request return to objective |
| Scope creep | Record only - Address in next task | | Scope creep | REJECT - Deletions outside task order must be reverted |
### 9. Improvement Suggestion Check ### 9. Improvement Suggestion Check

View File

@ -1,5 +1,12 @@
name: backend-cqrs name: backend-cqrs
description: CQRS+ES, Security, QA Expert Review description: CQRS+ES, Security, QA Expert Review
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 30 max_movements: 30
initial_movement: plan initial_movement: plan
movements: movements:

View File

@ -1,5 +1,12 @@
name: backend name: backend
description: Backend, Security, QA Expert Review description: Backend, Security, QA Expert Review
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 30 max_movements: 30
initial_movement: plan initial_movement: plan
movements: movements:

View File

@ -1,5 +1,12 @@
name: coding name: coding
description: Lightweight development piece with planning and parallel reviews (plan -> implement -> parallel review -> complete) description: Lightweight development piece with planning and parallel reviews (plan -> implement -> parallel review -> complete)
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 20 max_movements: 20
initial_movement: plan initial_movement: plan
movements: movements:

View File

@ -1,5 +1,12 @@
name: compound-eye name: compound-eye
description: Multi-model review - send the same instruction to Claude and Codex simultaneously, synthesize both responses description: Multi-model review - send the same instruction to Claude and Codex simultaneously, synthesize both responses
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 10 max_movements: 10
initial_movement: evaluate initial_movement: evaluate
movements: movements:

View File

@ -1,5 +1,12 @@
name: default name: default
description: Standard development piece with planning and specialized reviews description: Standard development piece with planning and specialized reviews
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 30 max_movements: 30
initial_movement: plan initial_movement: plan
loop_monitors: loop_monitors:

View File

@ -1,5 +1,12 @@
name: e2e-test name: e2e-test
description: E2E test focused piece (E2E analysis → E2E implementation → review → fix) description: E2E test focused piece (E2E analysis → E2E implementation → review → fix)
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 20 max_movements: 20
initial_movement: plan_test initial_movement: plan_test
loop_monitors: loop_monitors:

View File

@ -1,5 +1,12 @@
name: expert-cqrs name: expert-cqrs
description: CQRS+ES, Frontend, Security, QA Expert Review description: CQRS+ES, Frontend, Security, QA Expert Review
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 30 max_movements: 30
initial_movement: plan initial_movement: plan
movements: movements:

View File

@ -1,5 +1,12 @@
name: expert name: expert
description: Architecture, Frontend, Security, QA Expert Review description: Architecture, Frontend, Security, QA Expert Review
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 30 max_movements: 30
initial_movement: plan initial_movement: plan
movements: movements:

View File

@ -1,5 +1,12 @@
name: frontend name: frontend
description: Frontend, Security, QA Expert Review description: Frontend, Security, QA Expert Review
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 30 max_movements: 30
initial_movement: plan initial_movement: plan
movements: movements:

View File

@ -1,5 +1,12 @@
name: magi name: magi
description: MAGI Deliberation System - Analyze from 3 perspectives and decide by majority description: MAGI Deliberation System - Analyze from 3 perspectives and decide by majority
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 5 max_movements: 5
initial_movement: melchior initial_movement: melchior
movements: movements:

View File

@ -1,5 +1,12 @@
name: minimal name: minimal
description: Minimal development piece (implement -> parallel review -> fix if needed -> complete) description: Minimal development piece (implement -> parallel review -> fix if needed -> complete)
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 20 max_movements: 20
initial_movement: implement initial_movement: implement
movements: movements:

View File

@ -1,5 +1,12 @@
name: passthrough name: passthrough
description: Single-agent thin wrapper. Pass task directly to coder as-is. description: Single-agent thin wrapper. Pass task directly to coder as-is.
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 10 max_movements: 10
initial_movement: execute initial_movement: execute
movements: movements:

View File

@ -1,5 +1,12 @@
name: research name: research
description: Research piece - autonomously executes research without asking questions description: Research piece - autonomously executes research without asking questions
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 10 max_movements: 10
initial_movement: plan initial_movement: plan
movements: movements:

View File

@ -1,5 +1,12 @@
name: review-fix-minimal name: review-fix-minimal
description: Review and fix piece for existing code (starts with review, no implementation) description: Review and fix piece for existing code (starts with review, no implementation)
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 20 max_movements: 20
initial_movement: reviewers initial_movement: reviewers
movements: movements:

View File

@ -1,5 +1,12 @@
name: review-only name: review-only
description: Review-only piece - reviews code without making edits description: Review-only piece - reviews code without making edits
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 10 max_movements: 10
initial_movement: plan initial_movement: plan
movements: movements:

View File

@ -1,5 +1,12 @@
name: structural-reform name: structural-reform
description: Full project review and structural reform - iterative codebase restructuring with staged file splits description: Full project review and structural reform - iterative codebase restructuring with staged file splits
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 50 max_movements: 50
initial_movement: review initial_movement: review
loop_monitors: loop_monitors:

View File

@ -1,5 +1,12 @@
name: unit-test name: unit-test
description: Unit test focused piece (test analysis → test implementation → review → fix) description: Unit test focused piece (test analysis → test implementation → review → fix)
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 20 max_movements: 20
initial_movement: plan_test initial_movement: plan_test
loop_monitors: loop_monitors:

View File

@ -11,8 +11,14 @@
小規模タスクの場合は設計セクションを省略してください。 小規模タスクの場合は設計セクションを省略してください。
**やること:** **やること:**
1. タスクの要件を理解する 1. **参照資料の読み込み(必須・最初に実行)**
2. コードを調査して不明点を解決する - タスク指示書の「参照資料」セクションに記載されたファイル・ディレクトリを **Read/Glob で実際に開いて内容を確認する**
3. 影響範囲を特定する - ディレクトリが指定されている場合は中身を列挙し、該当ファイルを特定してから読む
4. ファイル構成・設計パターンを決定する(必要な場合) - 参照資料が存在しない・見つからない場合はその旨を報告し、推測で代用しない
5. 実装アプローチを決める - **指示書に明記されていない別ファイルを「参照資料の代わり」として使うことは禁止**
2. タスクの要件を理解する
- 参照資料の内容と現在の実装を突き合わせて差分を特定する
3. コードを調査して不明点を解決する
4. 影響範囲を特定する
5. ファイル構成・設計パターンを決定する(必要な場合)
6. 実装アプローチを決める

View File

@ -35,3 +35,4 @@
- 後方互換・Legacy 対応を勝手に追加する → 絶対禁止 - 後方互換・Legacy 対応を勝手に追加する → 絶対禁止
- リファクタリングで置き換えたコード・エクスポートを残す → 禁止(明示的に残すよう指示されない限り削除する) - リファクタリングで置き換えたコード・エクスポートを残す → 禁止(明示的に残すよう指示されない限り削除する)
- 根本原因を修正した上で安全機構を迂回するワークアラウンドを重ねる → 禁止 - 根本原因を修正した上で安全機構を迂回するワークアラウンドを重ねる → 禁止
- タスク指示書にない既存機能の削除・構造変更を「ついでに」行う → 禁止(計画に含まれていても、指示書に根拠がない大規模削除は報告する)

View File

@ -43,6 +43,23 @@
| 非機能要件 | パフォーマンス、セキュリティ等は満たされているか | | 非機能要件 | パフォーマンス、セキュリティ等は満たされているか |
| スコープ | 要求以上のことをしていないか(スコープクリープ) | | スコープ | 要求以上のことをしていないか(スコープクリープ) |
### スコープクリープの検出(削除は最重要チェック)
ファイルの**削除**と既存機能の**除去**はスコープクリープの最も危険な形態。
追加は元に戻せるが、削除されたフローの復元は困難。
**必須手順:**
1. 変更差分から削除されたファイルDと削除されたクラス・メソッド・エンドポイントを列挙する
2. 各削除がタスク指示書のどの項目に対応するかを照合する
3. タスク指示書に根拠がない削除は REJECT する
**典型的なスコープクリープ:**
- 「ステータス変更」タスクで Saga やエンドポイントが丸ごと削除されている
- 「UI修正」タスクでバックエンドのドメインモデルが構造変更されている
- 「表示変更」タスクでビジネスロジックのフローが書き換えられている
レビュアーが「設計判断として妥当」と承認していても、タスク指示書のスコープ外であれば REJECT する。
### リスク評価 ### リスク評価
| 影響度\発生確率 | 低 | 中 | 高 | | 影響度\発生確率 | 低 | 中 | 高 |

View File

@ -25,6 +25,17 @@
## ドメイン知識 ## ドメイン知識
### 情報の優先順位
タスク指示書に「参照資料」が指定されている場合、**そのファイルが唯一のソース・オブ・トゥルース**である。
類似の情報を含む別ファイルが存在しても、指示書が指定したファイルを優先する。
| 優先度 | ソース |
|--------|--------|
| **最優先** | タスク指示書の「参照資料」で指定されたファイル |
| 次点 | 実際のソースコード(現在の実装) |
| 参考 | その他のドキュメント |
### 情報の裏取り(ファクトチェック) ### 情報の裏取り(ファクトチェック)
分析で使用する情報は必ずソース・オブ・トゥルースで裏取りする。 分析で使用する情報は必ずソース・オブ・トゥルースで裏取りする。
@ -35,6 +46,7 @@
| 設定値・名前 | 実際の設定ファイル・定義ファイル | | 設定値・名前 | 実際の設定ファイル・定義ファイル |
| API・コマンド | 実際の実装コード | | API・コマンド | 実際の実装コード |
| データ構造・型 | 型定義ファイル・スキーマ | | データ構造・型 | 型定義ファイル・スキーマ |
| デザイン仕様 | タスク指示書で指定された参照ファイル |
### 構造設計 ### 構造設計
@ -52,8 +64,19 @@
- 循環依存を作らない - 循環依存を作らない
- 責務の分離(読み取りと書き込み、ビジネスロジックと IO - 責務の分離(読み取りと書き込み、ビジネスロジックと IO
### スコープ規律
タスク指示書に明記された作業のみを計画する。暗黙の「改善」を勝手に含めない。
**削除の判断基準:**
- **今回の変更で新たに未使用になったコード** → 削除を計画してよい(例: リネームした旧変数)
- **既存の機能・フロー・エンドポイント・Saga・イベント** → タスク指示書で明示的に指示されない限り削除しない
「ステータスを5つに変更する」は「enum値を書き換える」であり、「不要になったフローを丸ごと削除する」ではない。
タスク指示書の文言を拡大解釈しない。書かれていることだけを計画する。
### 計画の原則 ### 計画の原則
- 後方互換コードは計画に含めない(明示的な指示がない限り不要) - 後方互換コードは計画に含めない(明示的な指示がない限り不要)
- 使われていないものは削除する計画を立てる - 今回の変更で新たに未使用になったコードは削除する計画を立てる
- TODO コメントで済ませる計画は立てない。今やるか、やらないか - TODO コメントで済ませる計画は立てない。今やるか、やらないか

View File

@ -102,6 +102,21 @@
「機能的に無害」は免罪符ではない。修正コストがほぼゼロの指摘を「非ブロッキング」「次回タスク」に分類することは妥協である。レビュアーが発見し、数分以内に修正できる問題は今回のタスクで修正させる。 「機能的に無害」は免罪符ではない。修正コストがほぼゼロの指摘を「非ブロッキング」「次回タスク」に分類することは妥協である。レビュアーが発見し、数分以内に修正できる問題は今回のタスクで修正させる。
### スコープクリープの検出(削除は最重要チェック)
ファイルの**削除**と既存機能の**除去**はスコープクリープの最も危険な形態。
追加は元に戻せるが、削除されたフローの復元は困難。
**必須手順:**
1. 変更差分から削除されたファイルDと削除されたクラス・メソッド・エンドポイントを列挙する
2. 各削除がタスク指示書のどの項目に対応するかを照合する
3. タスク指示書に根拠がない削除は REJECT する
**典型的なスコープクリープ:**
- 「ステータス変更」タスクで Saga やエンドポイントが丸ごと削除されている
- 「UI修正」タスクでバックエンドのドメインモデルが構造変更されている
- 「表示変更」タスクでビジネスロジックのフローが書き換えられている
### ピース全体の見直し ### ピース全体の見直し
レポートディレクトリ内の全レポートを確認し、ピース全体の整合性をチェックする。 レポートディレクトリ内の全レポートを確認し、ピース全体の整合性をチェックする。

View File

@ -1,5 +1,12 @@
name: backend-cqrs name: backend-cqrs
description: CQRS+ES・セキュリティ・QA専門家レビュー description: CQRS+ES・セキュリティ・QA専門家レビュー
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 30 max_movements: 30
initial_movement: plan initial_movement: plan
movements: movements:

View File

@ -1,5 +1,12 @@
name: backend name: backend
description: バックエンド・セキュリティ・QA専門家レビュー description: バックエンド・セキュリティ・QA専門家レビュー
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 30 max_movements: 30
initial_movement: plan initial_movement: plan
movements: movements:

View File

@ -1,5 +1,12 @@
name: coding name: coding
description: Lightweight development piece with planning and parallel reviews (plan -> implement -> parallel review -> complete) description: Lightweight development piece with planning and parallel reviews (plan -> implement -> parallel review -> complete)
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 20 max_movements: 20
initial_movement: plan initial_movement: plan
movements: movements:

View File

@ -1,5 +1,12 @@
name: compound-eye name: compound-eye
description: 複眼レビュー - 同じ指示を Claude と Codex に同時に投げ、両者の回答を統合する description: 複眼レビュー - 同じ指示を Claude と Codex に同時に投げ、両者の回答を統合する
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 10 max_movements: 10
initial_movement: evaluate initial_movement: evaluate

View File

@ -1,5 +1,12 @@
name: default name: default
description: Standard development piece with planning and specialized reviews description: Standard development piece with planning and specialized reviews
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 30 max_movements: 30
initial_movement: plan initial_movement: plan
loop_monitors: loop_monitors:

View File

@ -1,5 +1,12 @@
name: e2e-test name: e2e-test
description: E2Eテスト追加に特化したピースE2E分析→E2E実装→レビュー→修正 description: E2Eテスト追加に特化したピースE2E分析→E2E実装→レビュー→修正
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 20 max_movements: 20
initial_movement: plan_test initial_movement: plan_test
loop_monitors: loop_monitors:

View File

@ -1,5 +1,12 @@
name: expert-cqrs name: expert-cqrs
description: CQRS+ES・フロントエンド・セキュリティ・QA専門家レビュー description: CQRS+ES・フロントエンド・セキュリティ・QA専門家レビュー
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 30 max_movements: 30
initial_movement: plan initial_movement: plan
movements: movements:

View File

@ -1,5 +1,12 @@
name: expert name: expert
description: アーキテクチャ・フロントエンド・セキュリティ・QA専門家レビュー description: アーキテクチャ・フロントエンド・セキュリティ・QA専門家レビュー
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 30 max_movements: 30
initial_movement: plan initial_movement: plan
movements: movements:

View File

@ -1,5 +1,12 @@
name: frontend name: frontend
description: フロントエンド・セキュリティ・QA専門家レビュー description: フロントエンド・セキュリティ・QA専門家レビュー
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 30 max_movements: 30
initial_movement: plan initial_movement: plan
movements: movements:

View File

@ -1,5 +1,12 @@
name: magi name: magi
description: MAGI合議システム - 3つの観点から分析し多数決で判定 description: MAGI合議システム - 3つの観点から分析し多数決で判定
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 5 max_movements: 5
initial_movement: melchior initial_movement: melchior
movements: movements:

View File

@ -1,5 +1,12 @@
name: minimal name: minimal
description: Minimal development piece (implement -> parallel review -> fix if needed -> complete) description: Minimal development piece (implement -> parallel review -> fix if needed -> complete)
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 20 max_movements: 20
initial_movement: implement initial_movement: implement
movements: movements:

View File

@ -1,5 +1,12 @@
name: passthrough name: passthrough
description: Single-agent thin wrapper. Pass task directly to coder as-is. description: Single-agent thin wrapper. Pass task directly to coder as-is.
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 10 max_movements: 10
initial_movement: execute initial_movement: execute
movements: movements:

View File

@ -1,5 +1,12 @@
name: research name: research
description: 調査ピース - 質問せずに自律的に調査を実行 description: 調査ピース - 質問せずに自律的に調査を実行
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 10 max_movements: 10
initial_movement: plan initial_movement: plan
movements: movements:

View File

@ -1,5 +1,12 @@
name: review-fix-minimal name: review-fix-minimal
description: 既存コードのレビューと修正ピース(レビュー開始、実装なし) description: 既存コードのレビューと修正ピース(レビュー開始、実装なし)
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 20 max_movements: 20
initial_movement: reviewers initial_movement: reviewers
movements: movements:

View File

@ -1,5 +1,12 @@
name: review-only name: review-only
description: レビュー専用ピース - コードをレビューするだけで編集は行わない description: レビュー専用ピース - コードをレビューするだけで編集は行わない
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 10 max_movements: 10
initial_movement: plan initial_movement: plan
movements: movements:

View File

@ -1,5 +1,12 @@
name: structural-reform name: structural-reform
description: プロジェクト全体レビューと構造改革 - 段階的なファイル分割による反復的コードベース再構築 description: プロジェクト全体レビューと構造改革 - 段階的なファイル分割による反復的コードベース再構築
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 50 max_movements: 50
initial_movement: review initial_movement: review
loop_monitors: loop_monitors:

View File

@ -1,5 +1,12 @@
name: unit-test name: unit-test
description: 単体テスト追加に特化したピース(テスト分析→テスト実装→レビュー→修正) description: 単体テスト追加に特化したピース(テスト分析→テスト実装→レビュー→修正)
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 20 max_movements: 20
initial_movement: plan_test initial_movement: plan_test
loop_monitors: loop_monitors:

View File

@ -803,10 +803,22 @@ rules:
| `provider` | - | このムーブメントのプロバイダーを上書き(`claude``codex`、または`opencode` | | `provider` | - | このムーブメントのプロバイダーを上書き(`claude``codex`、または`opencode` |
| `model` | - | このムーブメントのモデルを上書き | | `model` | - | このムーブメントのモデルを上書き |
| `permission_mode` | - | パーミッションモード: `readonly``edit``full`(プロバイダー非依存) | | `permission_mode` | - | パーミッションモード: `readonly``edit``full`(プロバイダー非依存) |
| `provider_options` | - | プロバイダー固有オプション(例: `codex.network_access``opencode.network_access` |
| `output_contracts` | - | レポートファイルの出力契約定義 | | `output_contracts` | - | レポートファイルの出力契約定義 |
| `quality_gates` | - | ムーブメント完了要件のAIディレクティブ | | `quality_gates` | - | ムーブメント完了要件のAIディレクティブ |
| `mcp_servers` | - | MCPModel Context Protocolサーバー設定stdio/SSE/HTTP | | `mcp_servers` | - | MCPModel Context Protocolサーバー設定stdio/SSE/HTTP |
ピース全体のデフォルトは `piece_config.provider_options` で設定でき、ムーブメント側 `provider_options` で上書きできます。
```yaml
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
```
## API使用例 ## API使用例
```typescript ```typescript

View File

@ -1,5 +1,12 @@
name: e2e-cycle-detect name: e2e-cycle-detect
description: Piece with loop_monitors for cycle detection E2E testing description: Piece with loop_monitors for cycle detection E2E testing
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 20 max_movements: 20
initial_movement: review initial_movement: review

View File

@ -1,5 +1,12 @@
name: e2e-mock-max-iter name: e2e-mock-max-iter
description: Piece with max_movements=2 that loops between two steps description: Piece with max_movements=2 that loops between two steps
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 2 max_movements: 2

View File

@ -1,5 +1,12 @@
name: e2e-mock-no-match name: e2e-mock-no-match
description: Piece with a strict rule condition that will not match mock output description: Piece with a strict rule condition that will not match mock output
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 3 max_movements: 3

View File

@ -1,5 +1,12 @@
name: e2e-mock-single name: e2e-mock-single
description: Minimal mock-only piece for CLI E2E description: Minimal mock-only piece for CLI E2E
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 3 max_movements: 3

View File

@ -1,5 +1,12 @@
name: e2e-mock-slow-multi-step name: e2e-mock-slow-multi-step
description: Multi-step mock piece to keep tasks in-flight long enough for SIGINT E2E description: Multi-step mock piece to keep tasks in-flight long enough for SIGINT E2E
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 20 max_movements: 20

View File

@ -1,5 +1,12 @@
name: e2e-mock-two-step name: e2e-mock-two-step
description: Two-step sequential piece for E2E testing description: Two-step sequential piece for E2E testing
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 5 max_movements: 5

View File

@ -1,5 +1,12 @@
name: e2e-multi-step-parallel name: e2e-multi-step-parallel
description: Multi-step piece with parallel sub-movements for E2E testing description: Multi-step piece with parallel sub-movements for E2E testing
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 10 max_movements: 10

View File

@ -1,5 +1,12 @@
name: e2e-report-judge name: e2e-report-judge
description: E2E piece that exercises report + judge phases description: E2E piece that exercises report + judge phases
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 3 max_movements: 3

View File

@ -1,5 +1,12 @@
name: e2e-simple name: e2e-simple
description: Minimal E2E test piece description: Minimal E2E test piece
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 5 max_movements: 5

View File

@ -1,5 +1,12 @@
name: e2e-structured-output name: e2e-structured-output
description: E2E piece to verify structured output rule matching description: E2E piece to verify structured output rule matching
piece_config:
provider_options:
codex:
network_access: true
opencode:
network_access: true
max_movements: 5 max_movements: 5

View File

@ -123,6 +123,7 @@ export function createIsolatedEnv(): IsolatedEnv {
TAKT_CONFIG_DIR: taktDir, TAKT_CONFIG_DIR: taktDir,
GIT_CONFIG_GLOBAL: gitConfigPath, GIT_CONFIG_GLOBAL: gitConfigPath,
TAKT_NO_TTY: '1', TAKT_NO_TTY: '1',
TAKT_NOTIFY_WEBHOOK: undefined,
}; };
return { return {

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "takt", "name": "takt",
"version": "0.13.0-alpha.1", "version": "0.13.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "takt", "name": "takt",
"version": "0.13.0-alpha.1", "version": "0.13.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.37", "@anthropic-ai/claude-agent-sdk": "^0.2.37",

View File

@ -1,6 +1,6 @@
{ {
"name": "takt", "name": "takt",
"version": "0.13.0-alpha.1", "version": "0.13.0",
"description": "TAKT: TAKT Agent Koordination Topology - AI Agent Piece Orchestration", "description": "TAKT: TAKT Agent Koordination Topology - AI Agent Piece Orchestration",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@ -14,11 +14,13 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
// ===== Codex SDK mock ===== // ===== Codex SDK mock =====
let mockEvents: Array<Record<string, unknown>> = []; let mockEvents: Array<Record<string, unknown>> = [];
let lastThreadOptions: Record<string, unknown> | undefined;
vi.mock('@openai/codex-sdk', () => { vi.mock('@openai/codex-sdk', () => {
return { return {
Codex: class MockCodex { Codex: class MockCodex {
async startThread() { async startThread(options?: Record<string, unknown>) {
lastThreadOptions = options;
return { return {
id: 'thread-mock', id: 'thread-mock',
runStreamed: async () => ({ runStreamed: async () => ({
@ -44,6 +46,7 @@ describe('CodexClient — structuredOutput 抽出', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
mockEvents = []; mockEvents = [];
lastThreadOptions = undefined;
}); });
it('outputSchema 指定時に agent_message の JSON テキストを structuredOutput として返す', async () => { it('outputSchema 指定時に agent_message の JSON テキストを structuredOutput として返す', async () => {
@ -149,4 +152,21 @@ describe('CodexClient — structuredOutput 抽出', () => {
expect(result.structuredOutput).toEqual({ step: 1 }); expect(result.structuredOutput).toEqual({ step: 1 });
}); });
it('provider_options.codex.network_access が ThreadOptions に反映される', async () => {
mockEvents = [
{ type: 'thread.started', thread_id: 'thread-1' },
{ type: 'turn.completed', usage: { input_tokens: 0, cached_input_tokens: 0, output_tokens: 0 } },
];
const client = new CodexClient();
await client.call('coder', 'prompt', {
cwd: '/tmp',
networkAccess: true,
});
expect(lastThreadOptions).toMatchObject({
networkAccessEnabled: true,
});
});
}); });

View File

@ -105,6 +105,54 @@ describe('PieceConfigRawSchema', () => {
expect(result.movements![0]?.permission_mode).toBe('edit'); expect(result.movements![0]?.permission_mode).toBe('edit');
}); });
it('should parse movement with provider_options', () => {
const config = {
name: 'test-piece',
movements: [
{
name: 'implement',
provider: 'codex',
provider_options: {
codex: { network_access: true },
opencode: { network_access: false },
},
instruction: '{task}',
},
],
};
const result = PieceConfigRawSchema.parse(config);
expect(result.movements![0]?.provider_options).toEqual({
codex: { network_access: true },
opencode: { network_access: false },
});
});
it('should parse piece-level piece_config.provider_options', () => {
const config = {
name: 'test-piece',
piece_config: {
provider_options: {
codex: { network_access: true },
},
},
movements: [
{
name: 'implement',
provider: 'codex',
instruction: '{task}',
},
],
};
const result = PieceConfigRawSchema.parse(config);
expect(result.piece_config).toEqual({
provider_options: {
codex: { network_access: true },
},
});
});
it('should allow omitting permission_mode', () => { it('should allow omitting permission_mode', () => {
const config = { const config = {
name: 'test-piece', name: 'test-piece',

View File

@ -84,4 +84,16 @@ describe('OpenCode permissions', () => {
action: 'deny', action: 'deny',
}); });
}); });
it('should force allow web tools when networkAccess=true', () => {
const ruleset = buildOpenCodePermissionRuleset('readonly', true);
expect(ruleset.find((rule) => rule.permission === 'webfetch')?.action).toBe('allow');
expect(ruleset.find((rule) => rule.permission === 'websearch')?.action).toBe('allow');
});
it('should force deny web tools when networkAccess=false', () => {
const ruleset = buildOpenCodePermissionRuleset('full', false);
expect(ruleset.find((rule) => rule.permission === 'webfetch')?.action).toBe('deny');
expect(ruleset.find((rule) => rule.permission === 'websearch')?.action).toBe('deny');
});
}); });

View File

@ -0,0 +1,46 @@
import { describe, expect, it } from 'vitest';
import { normalizePieceConfig } from '../infra/config/loaders/pieceParser.js';
describe('normalizePieceConfig provider_options', () => {
it('piece-level global を movement に継承し、movement 側で上書きできる', () => {
const raw = {
name: 'provider-options',
piece_config: {
provider_options: {
codex: { network_access: true },
opencode: { network_access: false },
},
},
movements: [
{
name: 'codex-default',
provider: 'codex',
instruction: '{task}',
},
{
name: 'codex-override',
provider: 'codex',
provider_options: {
codex: { network_access: false },
},
instruction: '{task}',
},
],
};
const config = normalizePieceConfig(raw, process.cwd());
expect(config.providerOptions).toEqual({
codex: { networkAccess: true },
opencode: { networkAccess: false },
});
expect(config.movements[0]?.providerOptions).toEqual({
codex: { networkAccess: true },
opencode: { networkAccess: false },
});
expect(config.movements[1]?.providerOptions).toEqual({
codex: { networkAccess: false },
opencode: { networkAccess: false },
});
});
});

View File

@ -107,6 +107,7 @@ export class AgentRunner {
maxTurns: options.maxTurns, maxTurns: options.maxTurns,
model: AgentRunner.resolveModel(resolvedProvider, options, agentConfig), model: AgentRunner.resolveModel(resolvedProvider, options, agentConfig),
permissionMode: options.permissionMode, permissionMode: options.permissionMode,
providerOptions: options.providerOptions,
onStream: options.onStream, onStream: options.onStream,
onPermissionRequest: options.onPermissionRequest, onPermissionRequest: options.onPermissionRequest,
onAskUserQuestion: options.onAskUserQuestion, onAskUserQuestion: options.onAskUserQuestion,

View File

@ -24,6 +24,11 @@ export interface RunAgentOptions {
maxTurns?: number; maxTurns?: number;
/** Permission mode for tool execution (from piece step) */ /** Permission mode for tool execution (from piece step) */
permissionMode?: PermissionMode; permissionMode?: PermissionMode;
/** Provider-specific movement options */
providerOptions?: {
codex?: { networkAccess?: boolean };
opencode?: { networkAccess?: boolean };
};
onStream?: StreamCallback; onStream?: StreamCallback;
onPermissionRequest?: PermissionHandler; onPermissionRequest?: PermissionHandler;
onAskUserQuestion?: AskUserQuestionHandler; onAskUserQuestion?: AskUserQuestionHandler;

View File

@ -80,6 +80,24 @@ export interface McpHttpServerConfig {
/** MCP server configuration (union of all YAML-configurable transports) */ /** MCP server configuration (union of all YAML-configurable transports) */
export type McpServerConfig = McpStdioServerConfig | McpSseServerConfig | McpHttpServerConfig; export type McpServerConfig = McpStdioServerConfig | McpSseServerConfig | McpHttpServerConfig;
/** Codex provider-specific options */
export interface CodexProviderOptions {
/** Enable network access for Codex workspace-write sandbox */
networkAccess?: boolean;
}
/** OpenCode provider-specific options */
export interface OpenCodeProviderOptions {
/** Enable/disable network tools (webfetch/websearch) */
networkAccess?: boolean;
}
/** Provider-specific movement options */
export interface MovementProviderOptions {
codex?: CodexProviderOptions;
opencode?: OpenCodeProviderOptions;
}
/** Single movement in a piece */ /** Single movement in a piece */
export interface PieceMovement { export interface PieceMovement {
name: string; name: string;
@ -103,6 +121,8 @@ export interface PieceMovement {
model?: string; model?: string;
/** Permission mode for tool execution in this movement */ /** Permission mode for tool execution in this movement */
permissionMode?: PermissionMode; permissionMode?: PermissionMode;
/** Provider-specific movement options */
providerOptions?: MovementProviderOptions;
/** Whether this movement is allowed to edit project files (true=allowed, false=prohibited, undefined=no prompt) */ /** Whether this movement is allowed to edit project files (true=allowed, false=prohibited, undefined=no prompt) */
edit?: boolean; edit?: boolean;
instructionTemplate: string; instructionTemplate: string;
@ -201,6 +221,8 @@ export interface LoopMonitorConfig {
export interface PieceConfig { export interface PieceConfig {
name: string; name: string;
description?: string; description?: string;
/** Piece-level default provider options (used as movement defaults) */
providerOptions?: MovementProviderOptions;
/** Persona definitions — map of name to file path or inline content (raw, not content-resolved) */ /** Persona definitions — map of name to file path or inline content (raw, not content-resolved) */
personas?: Record<string, string>; personas?: Record<string, string>;
/** Resolved policy definitions — map of name to file content (resolved at parse time) */ /** Resolved policy definitions — map of name to file content (resolved at parse time) */

View File

@ -59,6 +59,20 @@ export const StatusSchema = z.enum([
/** Permission mode schema for tool execution */ /** Permission mode schema for tool execution */
export const PermissionModeSchema = z.enum(['readonly', 'edit', 'full']); export const PermissionModeSchema = z.enum(['readonly', 'edit', 'full']);
/** Provider-specific movement options schema */
export const MovementProviderOptionsSchema = z.object({
codex: z.object({
network_access: z.boolean().optional(),
}).optional(),
opencode: z.object({
network_access: z.boolean().optional(),
}).optional(),
}).optional();
/** Piece-level provider options schema */
export const PieceProviderOptionsSchema = z.object({
provider_options: MovementProviderOptionsSchema,
}).optional();
/** /**
* Output contract item schema (new structured format). * Output contract item schema (new structured format).
@ -204,6 +218,7 @@ export const ParallelSubMovementRawSchema = z.object({
provider: z.enum(['claude', 'codex', 'opencode', 'mock']).optional(), provider: z.enum(['claude', 'codex', 'opencode', 'mock']).optional(),
model: z.string().optional(), model: z.string().optional(),
permission_mode: PermissionModeSchema.optional(), permission_mode: PermissionModeSchema.optional(),
provider_options: MovementProviderOptionsSchema,
edit: z.boolean().optional(), edit: z.boolean().optional(),
instruction: z.string().optional(), instruction: z.string().optional(),
instruction_template: z.string().optional(), instruction_template: z.string().optional(),
@ -235,6 +250,8 @@ export const PieceMovementRawSchema = z.object({
model: z.string().optional(), model: z.string().optional(),
/** Permission mode for tool execution in this movement */ /** Permission mode for tool execution in this movement */
permission_mode: PermissionModeSchema.optional(), permission_mode: PermissionModeSchema.optional(),
/** Provider-specific movement options */
provider_options: MovementProviderOptionsSchema,
/** Whether this movement is allowed to edit project files */ /** Whether this movement is allowed to edit project files */
edit: z.boolean().optional(), edit: z.boolean().optional(),
instruction: z.string().optional(), instruction: z.string().optional(),
@ -295,6 +312,7 @@ export const InteractiveModeSchema = z.enum(INTERACTIVE_MODES);
export const PieceConfigRawSchema = z.object({ export const PieceConfigRawSchema = z.object({
name: z.string().min(1), name: z.string().min(1),
description: z.string().optional(), description: z.string().optional(),
piece_config: PieceProviderOptionsSchema,
/** Piece-level persona definitions — map of name to .md file path or inline content */ /** Piece-level persona definitions — map of name to .md file path or inline content */
personas: z.record(z.string(), z.string()).optional(), personas: z.record(z.string(), z.string()).optional(),
/** Piece-level policy definitions — map of name to .md file path or inline content */ /** Piece-level policy definitions — map of name to .md file path or inline content */

View File

@ -38,6 +38,7 @@ export class OptionsBuilder {
provider: resolved.provider, provider: resolved.provider,
model: resolved.model, model: resolved.model,
permissionMode: step.permissionMode, permissionMode: step.permissionMode,
providerOptions: step.providerOptions,
language: this.getLanguage(), language: this.getLanguage(),
onStream: this.engineOptions.onStream, onStream: this.engineOptions.onStream,
onPermissionRequest: this.engineOptions.onPermissionRequest, onPermissionRequest: this.engineOptions.onPermissionRequest,

View File

@ -95,6 +95,7 @@ export class CodexClient {
...(options.model ? { model: options.model } : {}), ...(options.model ? { model: options.model } : {}),
workingDirectory: options.cwd, workingDirectory: options.cwd,
sandboxMode, sandboxMode,
...(options.networkAccess === undefined ? {} : { networkAccessEnabled: options.networkAccess }),
}; };
let threadId = options.sessionId; let threadId = options.sessionId;

View File

@ -27,6 +27,8 @@ export interface CodexCallOptions {
systemPrompt?: string; systemPrompt?: string;
/** Permission mode for sandbox configuration */ /** Permission mode for sandbox configuration */
permissionMode?: PermissionMode; permissionMode?: PermissionMode;
/** Enable network access for workspace-write sandbox */
networkAccess?: boolean;
/** Enable streaming mode with callback (best-effort) */ /** Enable streaming mode with callback (best-effort) */
onStream?: StreamCallback; onStream?: StreamCallback;
/** OpenAI API key (bypasses CLI auth) */ /** OpenAI API key (bypasses CLI auth) */

View File

@ -24,6 +24,36 @@ import {
type RawStep = z.output<typeof PieceMovementRawSchema>; type RawStep = z.output<typeof PieceMovementRawSchema>;
function normalizeProviderOptions(
raw: RawStep['provider_options'],
): PieceMovement['providerOptions'] {
if (!raw) return undefined;
const codex = raw.codex?.network_access === undefined
? undefined
: { networkAccess: raw.codex.network_access };
const opencode = raw.opencode?.network_access === undefined
? undefined
: { networkAccess: raw.opencode.network_access };
if (!codex && !opencode) return undefined;
return { ...(codex ? { codex } : {}), ...(opencode ? { opencode } : {}) };
}
function mergeProviderOptions(
base: PieceMovement['providerOptions'],
override: PieceMovement['providerOptions'],
): PieceMovement['providerOptions'] {
const codexNetworkAccess = override?.codex?.networkAccess ?? base?.codex?.networkAccess;
const opencodeNetworkAccess = override?.opencode?.networkAccess ?? base?.opencode?.networkAccess;
const codex = codexNetworkAccess === undefined ? undefined : { networkAccess: codexNetworkAccess };
const opencode = opencodeNetworkAccess === undefined ? undefined : { networkAccess: opencodeNetworkAccess };
if (!codex && !opencode) return undefined;
return { ...(codex ? { codex } : {}), ...(opencode ? { opencode } : {}) };
}
/** Check if a raw output contract item is the object form (has 'name' property). */ /** Check if a raw output contract item is the object form (has 'name' property). */
function isOutputContractItem(raw: unknown): raw is { name: string; order?: string; format?: string } { function isOutputContractItem(raw: unknown): raw is { name: string; order?: string; format?: string } {
return typeof raw === 'object' && raw !== null && !Array.isArray(raw) && 'name' in raw; return typeof raw === 'object' && raw !== null && !Array.isArray(raw) && 'name' in raw;
@ -209,6 +239,7 @@ function normalizeStepFromRaw(
step: RawStep, step: RawStep,
pieceDir: string, pieceDir: string,
sections: PieceSections, sections: PieceSections,
inheritedProviderOptions?: PieceMovement['providerOptions'],
context?: FacetResolutionContext, context?: FacetResolutionContext,
): PieceMovement { ): PieceMovement {
const rules: PieceRule[] | undefined = step.rules?.map(normalizeRule); const rules: PieceRule[] | undefined = step.rules?.map(normalizeRule);
@ -241,6 +272,7 @@ function normalizeStepFromRaw(
provider: step.provider, provider: step.provider,
model: step.model, model: step.model,
permissionMode: step.permission_mode, permissionMode: step.permission_mode,
providerOptions: mergeProviderOptions(inheritedProviderOptions, normalizeProviderOptions(step.provider_options)),
edit: step.edit, edit: step.edit,
instructionTemplate: (step.instruction_template instructionTemplate: (step.instruction_template
? resolveRefToContent(step.instruction_template, sections.resolvedInstructions, pieceDir, 'instructions', context) ? resolveRefToContent(step.instruction_template, sections.resolvedInstructions, pieceDir, 'instructions', context)
@ -254,7 +286,9 @@ function normalizeStepFromRaw(
}; };
if (step.parallel && step.parallel.length > 0) { if (step.parallel && step.parallel.length > 0) {
result.parallel = step.parallel.map((sub: RawStep) => normalizeStepFromRaw(sub, pieceDir, sections, context)); result.parallel = step.parallel.map((sub: RawStep) =>
normalizeStepFromRaw(sub, pieceDir, sections, inheritedProviderOptions, context),
);
} }
const arpeggioConfig = normalizeArpeggio(step.arpeggio, pieceDir); const arpeggioConfig = normalizeArpeggio(step.arpeggio, pieceDir);
@ -327,8 +361,10 @@ export function normalizePieceConfig(
resolvedReportFormats, resolvedReportFormats,
}; };
const pieceProviderOptions = normalizeProviderOptions(parsed.piece_config?.provider_options as RawStep['provider_options']);
const movements: PieceMovement[] = parsed.movements.map((step) => const movements: PieceMovement[] = parsed.movements.map((step) =>
normalizeStepFromRaw(step, pieceDir, sections, context), normalizeStepFromRaw(step, pieceDir, sections, pieceProviderOptions, context),
); );
// Schema guarantees movements.min(1) // Schema guarantees movements.min(1)
@ -337,6 +373,7 @@ export function normalizePieceConfig(
return { return {
name: parsed.name, name: parsed.name,
description: parsed.description, description: parsed.description,
providerOptions: pieceProviderOptions,
personas: parsed.personas, personas: parsed.personas,
policies: resolvedPolicies, policies: resolvedPolicies,
knowledge: resolvedKnowledge, knowledge: resolvedKnowledge,

View File

@ -311,7 +311,7 @@ export class OpenCodeClient {
const parsedModel = parseProviderModel(options.model, 'OpenCode model'); const parsedModel = parseProviderModel(options.model, 'OpenCode model');
const fullModel = `${parsedModel.providerID}/${parsedModel.modelID}`; const fullModel = `${parsedModel.providerID}/${parsedModel.modelID}`;
const port = await getFreePort(); const port = await getFreePort();
const permission = buildOpenCodePermissionConfig(options.permissionMode); const permission = buildOpenCodePermissionConfig(options.permissionMode, options.networkAccess);
const config = { const config = {
model: fullModel, model: fullModel,
small_model: fullModel, small_model: fullModel,
@ -332,7 +332,7 @@ export class OpenCodeClient {
? { data: { id: options.sessionId } } ? { data: { id: options.sessionId } }
: await client.session.create({ : await client.session.create({
directory: options.cwd, directory: options.cwd,
permission: buildOpenCodePermissionRuleset(options.permissionMode), permission: buildOpenCodePermissionRuleset(options.permissionMode, options.networkAccess),
}); });
const sessionId = sessionResult.data?.id; const sessionId = sessionResult.data?.id;

View File

@ -100,14 +100,38 @@ function buildPermissionMap(mode?: PermissionMode): OpenCodePermissionMap {
}; };
} }
export function buildOpenCodePermissionConfig(mode?: PermissionMode): OpenCodePermissionAction | Record<string, OpenCodePermissionAction> { function applyNetworkAccessOverride(
if (mode === 'readonly') return 'deny'; map: OpenCodePermissionMap,
if (mode === 'full') return 'allow'; networkAccess?: boolean,
return buildPermissionMap(mode); ): OpenCodePermissionMap {
if (networkAccess === undefined) {
return map;
}
const action: OpenCodePermissionAction = networkAccess ? 'allow' : 'deny';
return {
...map,
webfetch: action,
websearch: action,
};
} }
export function buildOpenCodePermissionRuleset(mode?: PermissionMode): Array<{ permission: string; pattern: string; action: OpenCodePermissionAction }> { export function buildOpenCodePermissionConfig(
const permissionMap = buildPermissionMap(mode); mode?: PermissionMode,
networkAccess?: boolean,
): OpenCodePermissionAction | Record<string, OpenCodePermissionAction> {
if (networkAccess === undefined) {
if (mode === 'readonly') return 'deny';
if (mode === 'full') return 'allow';
}
return applyNetworkAccessOverride(buildPermissionMap(mode), networkAccess);
}
export function buildOpenCodePermissionRuleset(
mode?: PermissionMode,
networkAccess?: boolean,
): Array<{ permission: string; pattern: string; action: OpenCodePermissionAction }> {
const permissionMap = applyNetworkAccessOverride(buildPermissionMap(mode), networkAccess);
return OPEN_CODE_PERMISSION_KEYS.map((permission) => ({ return OPEN_CODE_PERMISSION_KEYS.map((permission) => ({
permission, permission,
pattern: '**', pattern: '**',
@ -165,6 +189,8 @@ export interface OpenCodeCallOptions {
allowedTools?: string[]; allowedTools?: string[];
/** Permission mode for automatic permission handling */ /** Permission mode for automatic permission handling */
permissionMode?: PermissionMode; permissionMode?: PermissionMode;
/** Override network access (webfetch/websearch) */
networkAccess?: boolean;
/** Enable streaming mode with callback (best-effort) */ /** Enable streaming mode with callback (best-effort) */
onStream?: StreamCallback; onStream?: StreamCallback;
onAskUserQuestion?: AskUserQuestionHandler; onAskUserQuestion?: AskUserQuestionHandler;

View File

@ -31,6 +31,7 @@ function toCodexOptions(options: ProviderCallOptions): CodexCallOptions {
sessionId: options.sessionId, sessionId: options.sessionId,
model: options.model, model: options.model,
permissionMode: options.permissionMode, permissionMode: options.permissionMode,
networkAccess: options.providerOptions?.codex?.networkAccess,
onStream: options.onStream, onStream: options.onStream,
openaiApiKey: options.openaiApiKey ?? resolveOpenaiApiKey(), openaiApiKey: options.openaiApiKey ?? resolveOpenaiApiKey(),
outputSchema: options.outputSchema, outputSchema: options.outputSchema,

View File

@ -19,6 +19,7 @@ function toOpenCodeOptions(options: ProviderCallOptions): OpenCodeCallOptions {
model: options.model, model: options.model,
allowedTools: options.allowedTools, allowedTools: options.allowedTools,
permissionMode: options.permissionMode, permissionMode: options.permissionMode,
networkAccess: options.providerOptions?.opencode?.networkAccess,
onStream: options.onStream, onStream: options.onStream,
onAskUserQuestion: options.onAskUserQuestion, onAskUserQuestion: options.onAskUserQuestion,
opencodeApiKey: options.opencodeApiKey ?? resolveOpencodeApiKey(), opencodeApiKey: options.opencodeApiKey ?? resolveOpencodeApiKey(),

View File

@ -30,6 +30,11 @@ export interface ProviderCallOptions {
maxTurns?: number; maxTurns?: number;
/** Permission mode for tool execution (from piece step) */ /** Permission mode for tool execution (from piece step) */
permissionMode?: PermissionMode; permissionMode?: PermissionMode;
/** Provider-specific movement options */
providerOptions?: {
codex?: { networkAccess?: boolean };
opencode?: { networkAccess?: boolean };
};
onStream?: StreamCallback; onStream?: StreamCallback;
onPermissionRequest?: PermissionHandler; onPermissionRequest?: PermissionHandler;
onAskUserQuestion?: AskUserQuestionHandler; onAskUserQuestion?: AskUserQuestionHandler;