task-while 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -0
  3. package/bin/task-while.mjs +22 -0
  4. package/package.json +72 -0
  5. package/src/agents/claude.ts +175 -0
  6. package/src/agents/codex.ts +231 -0
  7. package/src/agents/provider-options.ts +45 -0
  8. package/src/agents/types.ts +69 -0
  9. package/src/batch/config.ts +109 -0
  10. package/src/batch/discovery.ts +35 -0
  11. package/src/batch/provider.ts +79 -0
  12. package/src/commands/batch.ts +266 -0
  13. package/src/commands/run.ts +270 -0
  14. package/src/core/engine-helpers.ts +114 -0
  15. package/src/core/engine-outcomes.ts +166 -0
  16. package/src/core/engine.ts +223 -0
  17. package/src/core/orchestrator-helpers.ts +52 -0
  18. package/src/core/orchestrator-integrate-resume.ts +149 -0
  19. package/src/core/orchestrator-review-resume.ts +228 -0
  20. package/src/core/orchestrator-task-attempt.ts +257 -0
  21. package/src/core/orchestrator.ts +99 -0
  22. package/src/core/runtime.ts +175 -0
  23. package/src/core/task-topology.ts +85 -0
  24. package/src/index.ts +121 -0
  25. package/src/prompts/implementer.ts +18 -0
  26. package/src/prompts/reviewer.ts +26 -0
  27. package/src/runtime/fs-runtime.ts +209 -0
  28. package/src/runtime/git.ts +137 -0
  29. package/src/runtime/github-pr-snapshot-decode.ts +307 -0
  30. package/src/runtime/github-pr-snapshot-queries.ts +137 -0
  31. package/src/runtime/github-pr-snapshot.ts +139 -0
  32. package/src/runtime/github.ts +232 -0
  33. package/src/runtime/path-layout.ts +13 -0
  34. package/src/runtime/workspace-resolver.ts +125 -0
  35. package/src/schema/index.ts +127 -0
  36. package/src/schema/model.ts +233 -0
  37. package/src/schema/shared.ts +93 -0
  38. package/src/task-sources/openspec/cli-json.ts +79 -0
  39. package/src/task-sources/openspec/context-files.ts +121 -0
  40. package/src/task-sources/openspec/parse-tasks-md.ts +57 -0
  41. package/src/task-sources/openspec/session.ts +235 -0
  42. package/src/task-sources/openspec/source.ts +59 -0
  43. package/src/task-sources/registry.ts +22 -0
  44. package/src/task-sources/spec-kit/parse-tasks-md.ts +48 -0
  45. package/src/task-sources/spec-kit/session.ts +174 -0
  46. package/src/task-sources/spec-kit/source.ts +30 -0
  47. package/src/task-sources/types.ts +47 -0
  48. package/src/types.ts +29 -0
  49. package/src/utils/fs.ts +31 -0
  50. package/src/workflow/config.ts +127 -0
  51. package/src/workflow/direct-preset.ts +44 -0
  52. package/src/workflow/finalize-task-checkbox.ts +24 -0
  53. package/src/workflow/preset.ts +86 -0
  54. package/src/workflow/pull-request-preset.ts +312 -0
  55. package/src/workflow/remote-reviewer.ts +243 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zhang Yu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,322 @@
1
+ # task-while
2
+
3
+ `task-while` is a git-first task orchestrator built around a task source protocol. The published package name and CLI binary are both `task-while`.
4
+
5
+ It reads workflow settings from `while.yaml`, opens the configured task source, executes one task at a time, reviews the result, integrates approved work, and creates one git commit per completed task. The built-in task sources are `spec-kit`, which consumes `spec.md`, `plan.md`, and `tasks.md` under `specs/<feature>/`, and `openspec`, which consumes an OpenSpec change under `openspec/changes/<change>/`.
6
+
7
+ It also provides a standalone `batch` command for YAML-driven file processing that is independent from the feature/task orchestration workflow.
8
+
9
+ ## Requirements
10
+
11
+ - Node.js 18 or newer
12
+ - For `run`: a git repository with an initial commit
13
+ - For `run`: a workspace with the directory layout required by the selected task source
14
+ - For `run`: the files required by the selected task source
15
+ - For `run`: a clean worktree before `run`
16
+
17
+ Current built-in source requirements:
18
+
19
+ - `task.source: spec-kit`
20
+ - `specs/<feature>/spec.md`
21
+ - `specs/<feature>/plan.md`
22
+ - `specs/<feature>/tasks.md`
23
+ - `task.source: openspec`
24
+ - `openspec/changes/<change>/proposal.md`
25
+ - `openspec/changes/<change>/design.md`
26
+ - `openspec/changes/<change>/tasks.md`
27
+ - At least one file under `openspec/changes/<change>/specs/**/*.md`
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pnpm add -D task-while
33
+ ```
34
+
35
+ Run it with:
36
+
37
+ ```bash
38
+ pnpm exec task-while run
39
+ ```
40
+
41
+ ## Configuration
42
+
43
+ `while.yaml` configures the `run` workflow only. When it is absent, the CLI runs `task.source: spec-kit`, `task.maxIterations: 5`, and `workflow.mode: direct` with `codex` for both roles. Each workflow role accepts provider-specific `model` and `effort`.
44
+
45
+ ```yaml
46
+ task:
47
+ source: spec-kit
48
+ maxIterations: 5
49
+
50
+ workflow:
51
+ mode: direct
52
+ roles:
53
+ implementer:
54
+ model: gpt-5-codex
55
+ effort: high
56
+ reviewer:
57
+ model: gpt-5-codex
58
+ effort: high
59
+ ```
60
+
61
+ Current status:
62
+
63
+ - `workflow.roles.<role>.provider` accepts `codex` or `claude`; when omitted it defaults to `codex`, including roles that only set `model` and/or `effort`
64
+ - `codex` `effort` accepts `minimal`, `low`, `medium`, `high`, or `xhigh`
65
+ - `claude` `effort` accepts `low`, `medium`, `high`, or `max`
66
+ - `workflow.mode: direct` requires `implementer` and `reviewer` to use identical `model` and `effort` when they share the same provider
67
+ - `workflow.mode: direct` uses a local reviewer
68
+ - `workflow.mode: pull-request` pushes a task branch, polls GitHub PR review from `chatgpt-codex-connector[bot]`, then squash-merges on approval
69
+ - in `workflow.mode: pull-request`, reviewer `provider` still selects the remote reviewer, but any local reviewer `model` and `effort` values are ignored
70
+ - `workflow.mode: pull-request` currently supports only `codex` as the remote reviewer provider
71
+ - `task.maxIterations` applies globally to every task in the selected source session
72
+
73
+ Example pull-request mode:
74
+
75
+ ```yaml
76
+ workflow:
77
+ mode: pull-request
78
+ roles:
79
+ implementer:
80
+ provider: claude
81
+ model: claude-sonnet-4-6
82
+ effort: max
83
+ reviewer:
84
+ provider: codex
85
+ ```
86
+
87
+ ## Workspace Resolution
88
+
89
+ `task-while run` resolves the current working directory as the workspace root.
90
+
91
+ - `task.source: spec-kit` requires `cwd/specs`
92
+ - `task.source: openspec` requires `cwd/openspec/changes`
93
+ - if the required source root is missing, the CLI fails with a clear user-facing error
94
+
95
+ Feature resolution order:
96
+
97
+ 1. `--feature`
98
+ 2. current git branch prefix for `spec-kit`
99
+ 3. the only entry under the selected source root
100
+
101
+ For `task.source: openspec`, `--feature` identifies the OpenSpec change id.
102
+
103
+ ## Commands
104
+
105
+ ### `task-while run`
106
+
107
+ Runs the current feature workflow from the existing `.while` state or initializes a new one. Run it from the workspace root so the current directory contains the source-specific root, such as `specs/` for `spec-kit` or `openspec/changes/` for `openspec`.
108
+
109
+ ```bash
110
+ cd /path/to/workspace
111
+ pnpm exec task-while run --feature 001-demo
112
+ ```
113
+
114
+ Useful flags:
115
+
116
+ - `--feature <featureId>`: select the feature explicitly
117
+ - For `task.source: openspec`, `--feature <featureId>` selects the OpenSpec change id
118
+ - `--until-task <taskSelector>`: stop after the target task reaches `done`
119
+ - `--verbose`: stream agent events to `stderr`
120
+
121
+ ### `task-while batch`
122
+
123
+ Runs a standalone YAML-driven batch job. This command does not read `while.yaml`, does not require `specs/`, and does not use the task-source workflow.
124
+
125
+ ```bash
126
+ cd /path/to/workspace
127
+ pnpm exec task-while batch --config ./batch.yaml
128
+ ```
129
+
130
+ Batch config example:
131
+
132
+ ```yaml
133
+ provider: claude
134
+ model: claude-sonnet-4-6
135
+ effort: max
136
+ glob:
137
+ - 'src/**/*.{ts,tsx}'
138
+ prompt: |
139
+ Read the target file and return structured output for it.
140
+ schema:
141
+ type: object
142
+ properties:
143
+ summary:
144
+ type: string
145
+ tags:
146
+ type: array
147
+ items:
148
+ type: string
149
+ required:
150
+ - summary
151
+ ```
152
+
153
+ Batch behavior:
154
+
155
+ - `glob` is optional and defaults to `**/*`
156
+ - `glob` is resolved relative to the directory that contains `batch.yaml`
157
+ - `provider`, `prompt`, and `schema` are required
158
+ - `model` and `effort` are optional and are forwarded to the selected provider client
159
+ - batch `provider` accepts `codex` or `claude`
160
+ - batch `codex` `effort` accepts `minimal`, `low`, `medium`, `high`, or `xhigh`
161
+ - batch `claude` `effort` accepts `low`, `medium`, `high`, or `max`
162
+ - each run scans files under the `batch.yaml` directory and filters them by `glob`
163
+ - execution state is written beside the YAML file in `state.json`
164
+ - structured results are written beside the YAML file in `results.json`
165
+ - result keys are relative to the directory that contains `batch.yaml`
166
+ - `--verbose` prints per-file failure reasons to `stderr`
167
+ - rerunning the command resumes unfinished work and skips files that already have accepted results
168
+ - when the current `pending` queue is exhausted and `failed` is non-empty, the command persists a recycle transition that moves `failed` back into `pending` for the next round
169
+ - the command exits only when both `pending` and `failed` are empty
170
+ - there is no retry limit for file-level failures; failed files continue to be retried round by round
171
+ - when `glob` matches no files, the command exits successfully without initializing a provider
172
+
173
+ ## Task Lifecycle
174
+
175
+ Each task follows this lifecycle:
176
+
177
+ 1. The implement role receives a task-source-built prompt for the current task.
178
+ 2. The reviewer evaluates the task-source-built review prompt plus changed-file context and overall risk.
179
+ 3. If review is approved, `task-while` asks the task source to apply its completion marker, creates the final integration commit, and records integrate artifacts under `.while`.
180
+
181
+ Completion requires all of the following:
182
+
183
+ - review verdict `pass`
184
+ - no findings
185
+ - every acceptance check passing
186
+
187
+ Review context uses `actualChangedFiles` derived from git diff against `HEAD`. In `pull-request` mode, changed-file context comes from the live PR snapshot instead of the local worktree diff.
188
+
189
+ In `pull-request` mode:
190
+
191
+ - review creates or reuses `task/<slug>` and an open PR against `main`
192
+ - if an open PR exists but the local task branch is missing, review restores the branch from `origin/task/<slug>`
193
+ - review creates a checkpoint commit with `checkpoint: Task <taskId>: <title> (attempt <n>)`
194
+ - review polls every minute with no default timeout
195
+ - review evaluates approval from a fully paginated live GraphQL PR snapshot
196
+ - approval is driven by the freshest `chatgpt-codex-connector[bot]` signal after the checkpoint commit
197
+ - active feedback includes unresolved, non-outdated review threads plus reviewer-authored review summaries and discussion comments after the current checkpoint
198
+ - process restart re-enters `review` or `integrate` and continues the same PR flow
199
+ - if the PR was already squash-merged before state was persisted, integrate treats it as already completed and finalizes local cleanup on resume
200
+ - integrate checks the task source completion marker, creates the final task commit when needed, squash-merges, returns to `main`, and deletes the local task branch
201
+
202
+ Completion is git-first:
203
+
204
+ - one completed task = one git commit
205
+ - `.while` is runtime state and is not committed
206
+ - completed task state stores `commitSha`
207
+
208
+ ## Built-in `spec-kit` Expectations
209
+
210
+ The built-in `spec-kit` task source parses raw Spec Kit task lines in file order. It does not require enhanced per-task metadata blocks.
211
+
212
+ Example:
213
+
214
+ ```md
215
+ ## Phase 1: Core
216
+
217
+ - [ ] T001 Implement greeting
218
+ - [ ] T002 [P] Implement farewell
219
+ - [ ] T010 [P] [US1] Add scenario coverage
220
+ ```
221
+
222
+ Current built-in `spec-kit` behavior:
223
+
224
+ - task ordering follows the order in `tasks.md`
225
+ - explicit task dependencies are not extracted from raw task lines
226
+ - implement/review prompts include the current task line, the current phase, `spec.md`, `plan.md`, and the full `tasks.md`
227
+ - completion is still written back through `tasks.md` checkboxes
228
+
229
+ ## Built-in `openspec` Expectations
230
+
231
+ The built-in `openspec` task source consumes an existing OpenSpec change directory and aligns implement/review prompts with `openspec instructions apply --json`.
232
+
233
+ Example configuration:
234
+
235
+ ```yaml
236
+ task:
237
+ source: openspec
238
+ maxIterations: 5
239
+ ```
240
+
241
+ Example run:
242
+
243
+ ```bash
244
+ pnpm exec task-while run --feature example-change
245
+ ```
246
+
247
+ Current built-in `openspec` behavior:
248
+
249
+ - `--feature` maps to `openspec/changes/<change>`
250
+ - stable task handles come from explicit numbering in `tasks.md`, such as `1.1` and `2.3`
251
+ - implement/review prompts include the current task, task group, `proposal.md`, `design.md`, expanded `specs/**/*.md`, full `tasks.md`, and the OpenSpec apply instruction/state/progress
252
+ - completion is still written by `task-while` after review/integrate success; it does not adopt `/opsx:apply`'s immediate checkbox update behavior
253
+ - `task-while` consumes OpenSpec artifacts and CLI JSON, but it does not run `/opsx:propose`
254
+
255
+ Task retry budget is configured globally in `while.yaml`:
256
+
257
+ ```yaml
258
+ task:
259
+ maxIterations: 2
260
+ ```
261
+
262
+ ## What `task-while` Does Not Do
263
+
264
+ `task-while` does not replace Spec Kit's project-level workflow. It does not run Spec Kit commands, checklists, hooks, or preset-installed skills.
265
+
266
+ Its contract with the selected task source is simple:
267
+
268
+ - the task source parses source artifacts and provides prompts plus completion operations
269
+ - `task-while` orchestrates implement, review, integrate, and persistence around that protocol
270
+
271
+ The standalone `batch` command is separate from this contract. It does not use task sources, task graphs, review/integrate stages, or git-first completion.
272
+
273
+ ## Runtime Layout
274
+
275
+ `run` keeps runtime state under:
276
+
277
+ ```text
278
+ <source-entry>/<id>/.while/
279
+ ```
280
+
281
+ Important files:
282
+
283
+ - `state.json`
284
+ - `graph.json`
285
+ - `report.json`
286
+ - `events.jsonl`
287
+ - `tasks/<taskHandle>/g<generation>/a<attempt>/implement.json`
288
+ - `tasks/<taskHandle>/g<generation>/a<attempt>/review.json`
289
+ - `tasks/<taskHandle>/g<generation>/a<attempt>/integrate.json`
290
+
291
+ `.while` is runtime state, not the long-term source of truth. Pull-request review recovery reloads persisted `implement` artifacts by `taskHandle`, `generation`, and `attempt`.
292
+
293
+ `batch` keeps runtime files beside the YAML config:
294
+
295
+ ```text
296
+ <config-dir>/
297
+ ├── batch.yaml
298
+ ├── state.json
299
+ └── results.json
300
+ ```
301
+
302
+ `state.json` contains:
303
+
304
+ - `pending`
305
+ - `inProgress`
306
+ - `failed`
307
+
308
+ `failed` is the current round's failure buffer. When `pending` becomes empty, those paths are persisted back into `pending` and retried in the next round. Historical state entries whose files no longer exist are dropped when a new run starts.
309
+
310
+ `results.json` maps accepted structured output by file path relative to the `batch.yaml` directory. If the config lives under a subdirectory and uses patterns such as `../input/*.txt`, the keys keep that relative form.
311
+
312
+ ## Publishing
313
+
314
+ Before publishing:
315
+
316
+ ```bash
317
+ pnpm lint
318
+ pnpm typecheck
319
+ AI_AGENT=1 pnpm test
320
+ AI_AGENT=1 pnpm tsx fixtures/smoke/codex-e2e.ts
321
+ npm pack --dry-run
322
+ ```
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process'
3
+ import { createRequire } from 'node:module'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
8
+ const require = createRequire(import.meta.url)
9
+ const entry = path.join(__dirname, '..', 'src', 'index.ts')
10
+ const tsxLoader = require.resolve('tsx')
11
+ const result = spawnSync(
12
+ process.execPath,
13
+ ['--import', tsxLoader, entry, ...process.argv.slice(2)],
14
+ {
15
+ stdio: 'inherit',
16
+ },
17
+ )
18
+
19
+ if (typeof result.status === 'number') {
20
+ process.exit(result.status)
21
+ }
22
+ process.exit(1)
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "task-while",
3
+ "version": "0.0.1",
4
+ "packageManager": "pnpm@10.32.1",
5
+ "description": "Git-first task orchestrator for task-source workspaces",
6
+ "author": "Zhang Yu",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/zhangyu1818/task-while#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/zhangyu1818/task-while.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/zhangyu1818/task-while/issues"
15
+ },
16
+ "keywords": [
17
+ "spec-kit",
18
+ "spec-driven",
19
+ "task-orchestrator",
20
+ "cli",
21
+ "git"
22
+ ],
23
+ "bin": {
24
+ "task-while": "bin/task-while.mjs"
25
+ },
26
+ "files": [
27
+ "LICENSE",
28
+ "README.md",
29
+ "bin",
30
+ "src"
31
+ ],
32
+ "engines": {
33
+ "node": ">=24"
34
+ },
35
+ "scripts": {
36
+ "dev": "tsx src/index.ts",
37
+ "format": "prettier . --write",
38
+ "format:check": "prettier . --check",
39
+ "lint": "eslint .",
40
+ "lint:fix": "eslint . --fix",
41
+ "smoke:codex": "tsx fixtures/smoke/codex-provider.ts",
42
+ "smoke:e2e:codex": "tsx fixtures/smoke/codex-e2e.ts",
43
+ "smoke:github-pr-snapshot": "tsx fixtures/smoke/github-pr-snapshot.ts",
44
+ "typecheck": "tsc --noEmit",
45
+ "test": "vitest run",
46
+ "test:watch": "vitest",
47
+ "coverage": "vitest run --coverage"
48
+ },
49
+ "dependencies": {
50
+ "@anthropic-ai/claude-agent-sdk": "^0.2.92",
51
+ "@openai/codex-sdk": "^0.116.0",
52
+ "ajv": "^8.18.0",
53
+ "arg": "^5.0.2",
54
+ "execa": "^8.0.1",
55
+ "fs-extra": "^11.3.4",
56
+ "glob": "13.0.6",
57
+ "tsx": "^4.21.0",
58
+ "yaml": "^2.8.3",
59
+ "zod": "3.25.76",
60
+ "zod-to-json-schema": "^3.25.1"
61
+ },
62
+ "devDependencies": {
63
+ "@types/fs-extra": "^11.0.4",
64
+ "@types/node": "^25.5.0",
65
+ "@vitest/coverage-v8": "4.1.0",
66
+ "@zhangyu1818/eslint-config": "^5.0.0",
67
+ "eslint": "^10.1.0",
68
+ "prettier": "^3.8.1",
69
+ "typescript": "^5.9.3",
70
+ "vitest": "4.1.0"
71
+ }
72
+ }
@@ -0,0 +1,175 @@
1
+ import { buildImplementerPrompt } from '../prompts/implementer'
2
+ import { buildReviewerPrompt } from '../prompts/reviewer'
3
+ import {
4
+ implementOutputSchema,
5
+ reviewOutputSchema,
6
+ validateImplementOutput,
7
+ validateReviewOutput,
8
+ } from '../schema/index'
9
+
10
+ import type { Options as ClaudeQueryOptions } from '@anthropic-ai/claude-agent-sdk'
11
+
12
+ import type { ClaudeProviderOptions } from './provider-options'
13
+ import type {
14
+ ImplementAgentInput,
15
+ ImplementerProvider,
16
+ ReviewAgentInput,
17
+ ReviewerProvider,
18
+ } from './types'
19
+
20
+ export interface ClaudeTextEvent {
21
+ delta: string
22
+ type: 'text'
23
+ }
24
+
25
+ export interface ClaudeAssistantEvent {
26
+ type: 'assistant'
27
+ }
28
+
29
+ export interface ClaudeResultEvent {
30
+ type: 'result'
31
+ }
32
+
33
+ export interface ClaudeErrorEvent {
34
+ message: string
35
+ type: 'error'
36
+ }
37
+
38
+ export type ClaudeAgentEvent =
39
+ | ClaudeAssistantEvent
40
+ | ClaudeErrorEvent
41
+ | ClaudeResultEvent
42
+ | ClaudeTextEvent
43
+
44
+ export type ClaudeAgentEventHandler = (event: ClaudeAgentEvent) => void
45
+
46
+ interface QueryResultMessage {
47
+ errors?: string[]
48
+ structured_output?: unknown
49
+ subtype: string
50
+ type: 'result'
51
+ }
52
+
53
+ interface QueryStreamEventMessage {
54
+ event: {
55
+ delta?: { text?: string; type?: string }
56
+ type: string
57
+ }
58
+ type: 'stream_event'
59
+ }
60
+
61
+ interface QueryAssistantMessage {
62
+ type: 'assistant'
63
+ }
64
+
65
+ type QueryMessage =
66
+ | QueryAssistantMessage
67
+ | QueryResultMessage
68
+ | QueryStreamEventMessage
69
+
70
+ export interface ClaudeAgentClientOptions extends ClaudeProviderOptions {
71
+ onEvent?: ClaudeAgentEventHandler
72
+ workspaceRoot: string
73
+ }
74
+
75
+ export interface ClaudeStructuredInput {
76
+ outputSchema: Record<string, unknown>
77
+ prompt: string
78
+ }
79
+
80
+ export class ClaudeAgentClient
81
+ implements ImplementerProvider, ReviewerProvider
82
+ {
83
+ public readonly name = 'claude'
84
+
85
+ public constructor(private readonly options: ClaudeAgentClientOptions) {}
86
+
87
+ private async collectStructuredOutput(
88
+ messages: AsyncIterable<QueryMessage>,
89
+ ): Promise<unknown> {
90
+ let structuredOutput: unknown = null
91
+
92
+ for await (const message of messages) {
93
+ if (message.type === 'stream_event' && this.options.onEvent) {
94
+ const event = message.event
95
+ if (
96
+ event.type === 'content_block_delta' &&
97
+ event.delta?.type === 'text_delta' &&
98
+ event.delta.text
99
+ ) {
100
+ this.options.onEvent({ delta: event.delta.text, type: 'text' })
101
+ }
102
+ }
103
+
104
+ if (message.type === 'assistant' && this.options.onEvent) {
105
+ this.options.onEvent({ type: 'assistant' })
106
+ }
107
+
108
+ if (message.type === 'result') {
109
+ if (message.subtype !== 'success') {
110
+ const detail = message.errors?.join('; ') ?? message.subtype
111
+ throw new Error(`Claude agent query failed: ${detail}`)
112
+ }
113
+ structuredOutput = message.structured_output ?? null
114
+ if (this.options.onEvent) {
115
+ this.options.onEvent({ type: 'result' })
116
+ }
117
+ }
118
+ }
119
+
120
+ if (structuredOutput === null || structuredOutput === undefined) {
121
+ throw new Error('Claude agent returned no structured output')
122
+ }
123
+
124
+ return structuredOutput
125
+ }
126
+
127
+ public async implement(input: ImplementAgentInput) {
128
+ const prompt = await buildImplementerPrompt(input)
129
+ const output = await this.invokeStructured<unknown>({
130
+ outputSchema: implementOutputSchema,
131
+ prompt,
132
+ })
133
+ return validateImplementOutput(output)
134
+ }
135
+
136
+ public async invokeStructured<T>(input: ClaudeStructuredInput): Promise<T> {
137
+ const { query } = await import('@anthropic-ai/claude-agent-sdk')
138
+ const queryOptions = {
139
+ allowDangerouslySkipPermissions: true,
140
+ cwd: this.options.workspaceRoot,
141
+ includePartialMessages: !!this.options.onEvent,
142
+ permissionMode: 'bypassPermissions',
143
+ outputFormat: {
144
+ schema: input.outputSchema,
145
+ type: 'json_schema',
146
+ },
147
+ ...(this.options.model ? { model: this.options.model } : {}),
148
+ ...(this.options.effort ? { effort: this.options.effort } : {}),
149
+ } satisfies ClaudeQueryOptions
150
+
151
+ const messages = query({
152
+ options: queryOptions,
153
+ prompt: input.prompt,
154
+ })
155
+
156
+ return this.collectStructuredOutput(
157
+ messages as AsyncIterable<QueryMessage>,
158
+ ) as Promise<T>
159
+ }
160
+
161
+ public async review(input: ReviewAgentInput) {
162
+ const prompt = await buildReviewerPrompt(input)
163
+ const output = await this.invokeStructured<unknown>({
164
+ outputSchema: reviewOutputSchema,
165
+ prompt,
166
+ })
167
+ return validateReviewOutput(output)
168
+ }
169
+ }
170
+
171
+ export function createClaudeProvider(
172
+ options: ClaudeAgentClientOptions,
173
+ ): ImplementerProvider & ReviewerProvider {
174
+ return new ClaudeAgentClient(options)
175
+ }