task-while 0.0.2 → 0.0.4

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 (41) hide show
  1. package/README.md +34 -34
  2. package/package.json +2 -2
  3. package/src/adapters/fs/harness-store.ts +84 -0
  4. package/src/agents/claude.ts +159 -9
  5. package/src/agents/codex.ts +68 -4
  6. package/src/agents/event-log.ts +160 -15
  7. package/src/batch/discovery.ts +1 -1
  8. package/src/commands/batch.ts +152 -155
  9. package/src/commands/run-branch-helpers.ts +81 -0
  10. package/src/commands/run-providers.ts +77 -0
  11. package/src/commands/run.ts +121 -177
  12. package/src/core/create-runtime-ports.ts +118 -0
  13. package/src/core/runtime.ts +15 -36
  14. package/src/harness/in-memory-store.ts +45 -0
  15. package/src/harness/kernel.ts +226 -0
  16. package/src/harness/state.ts +47 -0
  17. package/src/harness/store.ts +26 -0
  18. package/src/harness/workflow-builders.ts +87 -0
  19. package/src/harness/workflow-program.ts +86 -0
  20. package/src/ports/agent.ts +17 -0
  21. package/src/ports/code-host.ts +23 -0
  22. package/src/programs/batch.ts +139 -0
  23. package/src/programs/run-direct.ts +209 -0
  24. package/src/programs/run-pr-transitions.ts +81 -0
  25. package/src/programs/run-pr.ts +290 -0
  26. package/src/programs/shared-steps.ts +252 -0
  27. package/src/schedulers/scheduler.ts +208 -0
  28. package/src/session/session.ts +127 -0
  29. package/src/workflow/config.ts +15 -0
  30. package/src/core/engine-helpers.ts +0 -114
  31. package/src/core/engine-outcomes.ts +0 -166
  32. package/src/core/engine.ts +0 -223
  33. package/src/core/orchestrator-helpers.ts +0 -52
  34. package/src/core/orchestrator-integrate-resume.ts +0 -149
  35. package/src/core/orchestrator-review-resume.ts +0 -228
  36. package/src/core/orchestrator-task-attempt.ts +0 -257
  37. package/src/core/orchestrator.ts +0 -99
  38. package/src/runtime/fs-runtime.ts +0 -209
  39. package/src/workflow/direct-preset.ts +0 -44
  40. package/src/workflow/preset.ts +0 -86
  41. package/src/workflow/pull-request-preset.ts +0 -312
package/README.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # task-while
2
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`.
3
+ `task-while` is a git-first harness runtime built around a task source protocol. The published package name and CLI binary are both `task-while`.
4
4
 
5
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
6
 
7
- It also provides a standalone `batch` command for YAML-driven file processing that is independent from the feature/task orchestration workflow.
7
+ It also provides a standalone `batch` command for YAML-driven file processing that is independent from the feature/task harness runtime workflow.
8
8
 
9
9
  ## Requirements
10
10
 
11
- - Node.js 18 or newer
11
+ - Node.js 24 or newer
12
12
  - For `run`: a git repository with an initial commit
13
13
  - For `run`: a workspace with the directory layout required by the selected task source
14
14
  - For `run`: the files required by the selected task source
@@ -68,7 +68,7 @@ Current status:
68
68
  - `workflow.mode: pull-request` pushes a task branch, polls GitHub PR review from `chatgpt-codex-connector[bot]`, then squash-merges on approval
69
69
  - in `workflow.mode: pull-request`, reviewer `provider` still selects the remote reviewer, but any local reviewer `model` and `effort` values are ignored
70
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
71
+ - `task.maxIterations` uses the same configured limit for every task in the selected source session; run workflow retries share a single per-task budget across phases
72
72
 
73
73
  Example pull-request mode:
74
74
 
@@ -116,7 +116,7 @@ Useful flags:
116
116
  - `--feature <featureId>`: select the feature explicitly
117
117
  - For `task.source: openspec`, `--feature <featureId>` selects the OpenSpec change id
118
118
  - `--until-task <taskSelector>`: stop after the target task reaches `done`
119
- - `--verbose`: stream agent events to `stderr`
119
+ - `--verbose`: stream direct provider details to `stderr`, including Claude init/task/tool/result summaries and Codex thinking, commands, MCP tools, file updates, todo changes, messages, and final usage
120
120
 
121
121
  ### `task-while batch`
122
122
 
@@ -127,6 +127,8 @@ cd /path/to/workspace
127
127
  pnpm exec task-while batch --config ./batch.yaml
128
128
  ```
129
129
 
130
+ This repository also includes a repo-local skill at `skills/generate-batch-yaml/` for generating batch configs from natural-language requirements.
131
+
130
132
  Batch config example:
131
133
 
132
134
  ```yaml
@@ -160,14 +162,13 @@ Batch behavior:
160
162
  - batch `codex` `effort` accepts `minimal`, `low`, `medium`, `high`, or `xhigh`
161
163
  - batch `claude` `effort` accepts `low`, `medium`, `high`, or `max`
162
164
  - 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
165
  - structured results are written beside the YAML file in `results.json`
166
+ - internal harness state is written under `.while/harness/` beside the YAML file
165
167
  - result keys are relative to the directory that contains `batch.yaml`
166
- - `--verbose` streams provider agent events to `stderr` during batch execution
168
+ - `--verbose` streams batch-level progress and direct provider details to `stderr` during batch execution, including the current file, completion counts, Claude init/task/tool/result summaries, and Codex thinking, commands, MCP tools, file updates, todo changes, messages, and final usage
167
169
  - 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
170
+ - failed files are suspended and retried after all pending files are processed
171
+ - file-level retries are limited by `maxRetries` (default 3); exhausted files are marked blocked
171
172
  - when `glob` matches no files, the command exits successfully without initializing a provider
172
173
 
173
174
  ## Task Lifecycle
@@ -261,52 +262,51 @@ task:
261
262
 
262
263
  ## What `task-while` Does Not Do
263
264
 
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
+ `task-while` does not replace Spec Kit's project-level workflow. It does not run Spec Kit commands, checklists, or hooks.
265
266
 
266
267
  Its contract with the selected task source is simple:
267
268
 
268
269
  - 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
+ - the harness runtime drives implement, review, integrate, and persistence around that protocol
270
271
 
271
272
  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
 
274
+ ## Architecture
275
+
276
+ `task-while` uses a state-machine control plane:
277
+
278
+ - **TaskState** per subject is the single source of truth, written atomically as JSON
279
+ - **Transition log** (append-only JSONL) records phase transitions for debugging
280
+ - **Artifacts** store large structured outputs (contracts, reviews, implementations) separately
281
+ - A **pure kernel interpreter** executes typed workflow programs (action/gate/branch nodes + declarative transition tables)
282
+ - A **session layer** drives multi-subject scheduling via pluggable schedulers
283
+ - All external effects flow through unified **ports** (AgentPort, CodeHostPort, GitPort)
284
+
273
285
  ## Runtime Layout
274
286
 
275
287
  `run` keeps runtime state under:
276
288
 
277
289
  ```text
278
- <source-entry>/<id>/.while/
290
+ <source-entry>/<id>/.while/harness/
291
+ state/<protocol>/<subject-id>.json — TaskState per subject (truth)
292
+ transitions/<protocol>/<subject-id>.jsonl — TransitionRecord log (debug)
293
+ artifacts/<protocol>/<subject-id>/*.json — Artifact per kind/iteration
279
294
  ```
280
295
 
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`.
296
+ `.while` is runtime state, not the long-term source of truth. Resume reads the state file directly — no event replay needed.
292
297
 
293
298
  `batch` keeps runtime files beside the YAML config:
294
299
 
295
300
  ```text
296
301
  <config-dir>/
297
302
  ├── batch.yaml
298
- ├── state.json
299
- └── results.json
303
+ ├── results.json
304
+ └── .while/harness/
305
+ ├── state/batch/*.json
306
+ ├── transitions/batch/*.jsonl
307
+ └── artifacts/batch/...
300
308
  ```
301
309
 
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
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
311
 
312
312
  ## Publishing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "task-while",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "packageManager": "pnpm@10.32.1",
5
5
  "description": "Git-first task orchestrator for task-source workspaces",
6
6
  "author": "Zhang Yu",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "@anthropic-ai/claude-agent-sdk": "^0.2.92",
51
- "@openai/codex-sdk": "^0.116.0",
51
+ "@openai/codex-sdk": "^0.118.0",
52
52
  "ajv": "^8.18.0",
53
53
  "arg": "^5.0.2",
54
54
  "execa": "^8.0.1",
@@ -0,0 +1,84 @@
1
+ import { appendFile, mkdir, readdir, rename, writeFile } from 'node:fs/promises'
2
+ import path from 'node:path'
3
+
4
+ import { pathExists, readJson } from 'fs-extra'
5
+
6
+ import type { HarnessStore } from '../../harness/store'
7
+
8
+ function encodeArtifactFileName(artifactId: string) {
9
+ return encodeURIComponent(artifactId)
10
+ }
11
+
12
+ export function createFsHarnessStore(root: string): HarnessStore {
13
+ const stateFile = (protocol: string, subjectId: string) =>
14
+ path.join(root, 'state', protocol, `${encodeURIComponent(subjectId)}.json`)
15
+
16
+ const artifactFile = (
17
+ protocol: string,
18
+ subjectId: string,
19
+ artifactId: string,
20
+ ) =>
21
+ path.join(
22
+ root,
23
+ 'artifacts',
24
+ protocol,
25
+ encodeURIComponent(subjectId),
26
+ `${encodeArtifactFileName(artifactId)}.json`,
27
+ )
28
+
29
+ const artifactDir = (protocol: string, subjectId: string) =>
30
+ path.join(root, 'artifacts', protocol, encodeURIComponent(subjectId))
31
+
32
+ const transitionFile = (protocol: string, subjectId: string) =>
33
+ path.join(
34
+ root,
35
+ 'transitions',
36
+ protocol,
37
+ `${encodeURIComponent(subjectId)}.jsonl`,
38
+ )
39
+
40
+ return {
41
+ async appendTransition(protocol, subjectId, record) {
42
+ const file = transitionFile(protocol, subjectId)
43
+ await mkdir(path.dirname(file), { recursive: true })
44
+ await appendFile(file, `${JSON.stringify(record)}\n`)
45
+ },
46
+ async listArtifacts(protocol, subjectId) {
47
+ const dir = artifactDir(protocol, subjectId)
48
+ if (!(await pathExists(dir))) {
49
+ return []
50
+ }
51
+ const directoryEntries = await readdir(dir)
52
+ const entries = directoryEntries.filter((e) => e.endsWith('.json'))
53
+ return Promise.all(
54
+ entries.map((entry) => readJson(path.join(dir, entry))),
55
+ )
56
+ },
57
+ async loadArtifact(protocol, subjectId, artifactId) {
58
+ const file = artifactFile(protocol, subjectId, artifactId)
59
+ if (!(await pathExists(file))) {
60
+ return null
61
+ }
62
+ return readJson(file)
63
+ },
64
+ async loadState(protocol, subjectId) {
65
+ const file = stateFile(protocol, subjectId)
66
+ if (!(await pathExists(file))) {
67
+ return null
68
+ }
69
+ return readJson(file)
70
+ },
71
+ async saveArtifact(protocol, subjectId, artifact) {
72
+ const file = artifactFile(protocol, subjectId, artifact.id)
73
+ await mkdir(path.dirname(file), { recursive: true })
74
+ await writeFile(file, JSON.stringify(artifact, null, 2))
75
+ },
76
+ async saveState(protocol, subjectId, state) {
77
+ const file = stateFile(protocol, subjectId)
78
+ const tmpFile = `${file}.tmp`
79
+ await mkdir(path.dirname(file), { recursive: true })
80
+ await writeFile(tmpFile, JSON.stringify(state, null, 2))
81
+ await rename(tmpFile, file)
82
+ },
83
+ }
84
+ }
@@ -22,11 +22,45 @@ export interface ClaudeTextEvent {
22
22
  type: 'text'
23
23
  }
24
24
 
25
- export interface ClaudeAssistantEvent {
26
- type: 'assistant'
25
+ export interface ClaudeInitEvent {
26
+ mcpServers: { name: string; status: string }[]
27
+ model: string
28
+ permissionMode: string
29
+ skills: string[]
30
+ tools: string[]
31
+ type: 'system.init'
32
+ }
33
+
34
+ export interface ClaudeTaskStartedEvent {
35
+ description: string
36
+ taskId: string
37
+ type: 'task.started'
38
+ }
39
+
40
+ export interface ClaudeTaskProgressEvent {
41
+ description: string
42
+ lastToolName?: string
43
+ summary?: string
44
+ taskId: string
45
+ type: 'task.progress'
46
+ }
47
+
48
+ export interface ClaudeToolProgressEvent {
49
+ elapsedTimeSeconds: number
50
+ toolName: string
51
+ toolUseId: string
52
+ type: 'tool.progress'
53
+ }
54
+
55
+ export interface ClaudeToolSummaryEvent {
56
+ summary: string
57
+ type: 'tool.summary'
27
58
  }
28
59
 
29
60
  export interface ClaudeResultEvent {
61
+ durationMs: number
62
+ numTurns: number
63
+ subtype: 'success'
30
64
  type: 'result'
31
65
  }
32
66
 
@@ -36,15 +70,21 @@ export interface ClaudeErrorEvent {
36
70
  }
37
71
 
38
72
  export type ClaudeAgentEvent =
39
- | ClaudeAssistantEvent
40
73
  | ClaudeErrorEvent
74
+ | ClaudeInitEvent
41
75
  | ClaudeResultEvent
76
+ | ClaudeTaskProgressEvent
77
+ | ClaudeTaskStartedEvent
42
78
  | ClaudeTextEvent
79
+ | ClaudeToolProgressEvent
80
+ | ClaudeToolSummaryEvent
43
81
 
44
82
  export type ClaudeAgentEventHandler = (event: ClaudeAgentEvent) => void
45
83
 
46
84
  interface QueryResultMessage {
85
+ duration_ms?: number
47
86
  errors?: string[]
87
+ num_turns?: number
48
88
  structured_output?: unknown
49
89
  subtype: string
50
90
  type: 'result'
@@ -62,10 +102,53 @@ interface QueryAssistantMessage {
62
102
  type: 'assistant'
63
103
  }
64
104
 
105
+ interface QuerySystemInitMessage {
106
+ mcp_servers: { name: string; status: string }[]
107
+ model: string
108
+ permissionMode: string
109
+ skills: string[]
110
+ subtype: 'init'
111
+ tools: string[]
112
+ type: 'system'
113
+ }
114
+
115
+ interface QueryTaskStartedMessage {
116
+ description: string
117
+ subtype: 'task_started'
118
+ task_id: string
119
+ type: 'system'
120
+ }
121
+
122
+ interface QueryTaskProgressMessage {
123
+ description: string
124
+ last_tool_name?: string
125
+ subtype: 'task_progress'
126
+ summary?: string
127
+ task_id: string
128
+ type: 'system'
129
+ }
130
+
131
+ interface QueryToolProgressMessage {
132
+ elapsed_time_seconds: number
133
+ tool_name: string
134
+ tool_use_id: string
135
+ type: 'tool_progress'
136
+ }
137
+
138
+ interface QueryToolUseSummaryMessage {
139
+ summary: string
140
+ type: 'tool_use_summary'
141
+ }
142
+
65
143
  type QueryMessage =
66
144
  | QueryAssistantMessage
67
145
  | QueryResultMessage
68
146
  | QueryStreamEventMessage
147
+ | QuerySystemInitMessage
148
+ | QueryTaskProgressMessage
149
+ | QueryTaskStartedMessage
150
+ | QueryToolProgressMessage
151
+ | QueryToolUseSummaryMessage
69
152
 
70
153
  export interface ClaudeAgentClientOptions extends ClaudeProviderOptions {
71
154
  onEvent?: ClaudeAgentEventHandler
@@ -90,6 +173,65 @@ export class ClaudeAgentClient
90
173
  let structuredOutput: unknown = null
91
174
 
92
175
  for await (const message of messages) {
176
+ if (
177
+ message.type === 'system' &&
178
+ message.subtype === 'init' &&
179
+ this.options.onEvent
180
+ ) {
181
+ this.options.onEvent({
182
+ mcpServers: message.mcp_servers,
183
+ model: message.model,
184
+ permissionMode: message.permissionMode,
185
+ skills: message.skills,
186
+ tools: message.tools,
187
+ type: 'system.init',
188
+ })
189
+ }
190
+
191
+ if (
192
+ message.type === 'system' &&
193
+ message.subtype === 'task_started' &&
194
+ this.options.onEvent
195
+ ) {
196
+ this.options.onEvent({
197
+ description: message.description,
198
+ taskId: message.task_id,
199
+ type: 'task.started',
200
+ })
201
+ }
202
+
203
+ if (
204
+ message.type === 'system' &&
205
+ message.subtype === 'task_progress' &&
206
+ this.options.onEvent
207
+ ) {
208
+ this.options.onEvent({
209
+ description: message.description,
210
+ taskId: message.task_id,
211
+ type: 'task.progress',
212
+ ...(message.last_tool_name
213
+ ? { lastToolName: message.last_tool_name }
214
+ : {}),
215
+ ...(message.summary ? { summary: message.summary } : {}),
216
+ })
217
+ }
218
+
219
+ if (message.type === 'tool_progress' && this.options.onEvent) {
220
+ this.options.onEvent({
221
+ elapsedTimeSeconds: message.elapsed_time_seconds,
222
+ toolName: message.tool_name,
223
+ toolUseId: message.tool_use_id,
224
+ type: 'tool.progress',
225
+ })
226
+ }
227
+
228
+ if (message.type === 'tool_use_summary' && this.options.onEvent) {
229
+ this.options.onEvent({
230
+ summary: message.summary,
231
+ type: 'tool.summary',
232
+ })
233
+ }
234
+
93
235
  if (message.type === 'stream_event' && this.options.onEvent) {
94
236
  const event = message.event
95
237
  if (
@@ -101,10 +243,6 @@ export class ClaudeAgentClient
101
243
  }
102
244
  }
103
245
 
104
- if (message.type === 'assistant' && this.options.onEvent) {
105
- this.options.onEvent({ type: 'assistant' })
106
- }
107
-
108
246
  if (message.type === 'result') {
109
247
  if (message.subtype !== 'success') {
110
248
  const detail = message.errors?.join('; ') ?? message.subtype
@@ -112,7 +250,12 @@ export class ClaudeAgentClient
112
250
  }
113
251
  structuredOutput = message.structured_output ?? null
114
252
  if (this.options.onEvent) {
115
- this.options.onEvent({ type: 'result' })
253
+ this.options.onEvent({
254
+ durationMs: message.duration_ms ?? 0,
255
+ numTurns: message.num_turns ?? 0,
256
+ subtype: 'success',
257
+ type: 'result',
258
+ })
116
259
  }
117
260
  }
118
261
  }
@@ -138,12 +281,19 @@ export class ClaudeAgentClient
138
281
  const queryOptions = {
139
282
  allowDangerouslySkipPermissions: true,
140
283
  cwd: this.options.workspaceRoot,
141
- includePartialMessages: !!this.options.onEvent,
142
284
  permissionMode: 'bypassPermissions',
143
285
  outputFormat: {
144
286
  schema: input.outputSchema,
145
287
  type: 'json_schema',
146
288
  },
289
+ ...(this.options.onEvent
290
+ ? {
291
+ agentProgressSummaries: true,
292
+ includePartialMessages: true,
293
+ }
294
+ : {
295
+ includePartialMessages: false,
296
+ }),
147
297
  ...(this.options.model ? { model: this.options.model } : {}),
148
298
  ...(this.options.effort ? { effort: this.options.effort } : {}),
149
299
  } satisfies ClaudeQueryOptions
@@ -58,11 +58,75 @@ export interface CodexTurnFailedError {
58
58
  message: string
59
59
  }
60
60
 
61
- export interface CodexItemPayload {
62
- text?: string
63
- type: string
61
+ export interface CodexAgentMessageItem {
62
+ id: string
63
+ text: string
64
+ type: 'agent_message'
64
65
  }
65
66
 
67
+ export interface CodexReasoningItem {
68
+ id: string
69
+ text: string
70
+ type: 'reasoning'
71
+ }
72
+
73
+ export interface CodexCommandExecutionItem {
74
+ aggregated_output: string
75
+ command: string
76
+ exit_code?: number
77
+ id: string
78
+ status: 'completed' | 'failed' | 'in_progress'
79
+ type: 'command_execution'
80
+ }
81
+
82
+ export interface CodexFileChangeItem {
83
+ changes: { kind: 'add' | 'delete' | 'update'; path: string }[]
84
+ id: string
85
+ status: 'completed' | 'failed'
86
+ type: 'file_change'
87
+ }
88
+
89
+ export interface CodexMcpToolCallItem {
90
+ arguments: unknown
91
+ error?: { message: string }
92
+ id: string
93
+ result?: {
94
+ structured_content: unknown
95
+ }
96
+ server: string
97
+ status: 'completed' | 'failed' | 'in_progress'
98
+ tool: string
99
+ type: 'mcp_tool_call'
100
+ }
101
+
102
+ export interface CodexWebSearchItem {
103
+ id: string
104
+ query: string
105
+ type: 'web_search'
106
+ }
107
+
108
+ export interface CodexTodoListItem {
109
+ id: string
110
+ items: { completed: boolean; text: string }[]
111
+ type: 'todo_list'
112
+ }
113
+
114
+ export interface CodexErrorItem {
115
+ id: string
116
+ message: string
117
+ type: 'error'
118
+ }
119
+
120
+ export type CodexItemPayload =
121
+ | CodexAgentMessageItem
122
+ | CodexCommandExecutionItem
123
+ | CodexErrorItem
124
+ | CodexFileChangeItem
125
+ | CodexMcpToolCallItem
126
+ | CodexReasoningItem
127
+ | CodexTodoListItem
128
+ | CodexWebSearchItem
129
+
66
130
  export type CodexThreadEvent =
67
131
  | CodexErrorEvent
68
132
  | CodexItemEvent
@@ -147,7 +211,7 @@ export class CodexAgentClient implements ImplementerProvider, ReviewerProvider {
147
211
  event.type === 'item.completed' &&
148
212
  event.item.type === 'agent_message'
149
213
  ) {
150
- finalResponse = event.item.text?.trim() ?? ''
214
+ finalResponse = event.item.text.trim()
151
215
  }
152
216
  }
153
217