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.
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/bin/task-while.mjs +22 -0
- package/package.json +72 -0
- package/src/agents/claude.ts +175 -0
- package/src/agents/codex.ts +231 -0
- package/src/agents/provider-options.ts +45 -0
- package/src/agents/types.ts +69 -0
- package/src/batch/config.ts +109 -0
- package/src/batch/discovery.ts +35 -0
- package/src/batch/provider.ts +79 -0
- package/src/commands/batch.ts +266 -0
- package/src/commands/run.ts +270 -0
- package/src/core/engine-helpers.ts +114 -0
- package/src/core/engine-outcomes.ts +166 -0
- package/src/core/engine.ts +223 -0
- package/src/core/orchestrator-helpers.ts +52 -0
- package/src/core/orchestrator-integrate-resume.ts +149 -0
- package/src/core/orchestrator-review-resume.ts +228 -0
- package/src/core/orchestrator-task-attempt.ts +257 -0
- package/src/core/orchestrator.ts +99 -0
- package/src/core/runtime.ts +175 -0
- package/src/core/task-topology.ts +85 -0
- package/src/index.ts +121 -0
- package/src/prompts/implementer.ts +18 -0
- package/src/prompts/reviewer.ts +26 -0
- package/src/runtime/fs-runtime.ts +209 -0
- package/src/runtime/git.ts +137 -0
- package/src/runtime/github-pr-snapshot-decode.ts +307 -0
- package/src/runtime/github-pr-snapshot-queries.ts +137 -0
- package/src/runtime/github-pr-snapshot.ts +139 -0
- package/src/runtime/github.ts +232 -0
- package/src/runtime/path-layout.ts +13 -0
- package/src/runtime/workspace-resolver.ts +125 -0
- package/src/schema/index.ts +127 -0
- package/src/schema/model.ts +233 -0
- package/src/schema/shared.ts +93 -0
- package/src/task-sources/openspec/cli-json.ts +79 -0
- package/src/task-sources/openspec/context-files.ts +121 -0
- package/src/task-sources/openspec/parse-tasks-md.ts +57 -0
- package/src/task-sources/openspec/session.ts +235 -0
- package/src/task-sources/openspec/source.ts +59 -0
- package/src/task-sources/registry.ts +22 -0
- package/src/task-sources/spec-kit/parse-tasks-md.ts +48 -0
- package/src/task-sources/spec-kit/session.ts +174 -0
- package/src/task-sources/spec-kit/source.ts +30 -0
- package/src/task-sources/types.ts +47 -0
- package/src/types.ts +29 -0
- package/src/utils/fs.ts +31 -0
- package/src/workflow/config.ts +127 -0
- package/src/workflow/direct-preset.ts +44 -0
- package/src/workflow/finalize-task-checkbox.ts +24 -0
- package/src/workflow/preset.ts +86 -0
- package/src/workflow/pull-request-preset.ts +312 -0
- 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
|
+
}
|