pi-long-task 0.3.9 → 0.3.11
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/README.md +221 -5
- package/package.json +1 -1
- package/src/coordinator.ts +33 -7
- package/src/coverage_goal.ts +90 -0
- package/src/goal_discovery.ts +741 -0
- package/src/goal_loop.ts +586 -0
- package/src/goal_orchestrator.ts +399 -0
- package/src/goal_review.ts +616 -0
- package/src/goal_spec.ts +670 -0
- package/src/goal_state.ts +228 -0
- package/src/goal_todo_execution.ts +309 -0
- package/src/goal_todo_generation.ts +542 -0
- package/src/index.ts +89 -2
- package/src/input_router.ts +153 -6
- package/src/render.ts +238 -4
- package/src/todo_generator.ts +135 -7
- package/src/types.ts +74 -3
- package/src/worker_session.ts +23 -2
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Use it when a coding request is bigger than one focused interaction. Pi Long Tas
|
|
|
18
18
|
When you ask Pi to run a long task, Pi Long Task:
|
|
19
19
|
|
|
20
20
|
1. Recognizes natural-language requests like "run a long task with commits" and routes them to `pi_long_task`.
|
|
21
|
-
2. Creates or cleans up a TODO plan from your request
|
|
21
|
+
2. Creates or cleans up a TODO plan from your request, optionally guided by a high-level `goal`. Natural-language planning uses a bounded planner session; if generated TODO markdown is invalid, Pi Long Task asks the planner to repair it once before failing the run.
|
|
22
22
|
3. Works through each unfinished TODO task in order using isolated worker sessions.
|
|
23
23
|
4. Registers a Pi TUI sidebar/widget when UI support is available and updates it with the current task, inferred subtask progress, and full task timeline while the run is active.
|
|
24
24
|
5. Retries unfinished tasks up to the configured attempt limit.
|
|
@@ -26,6 +26,8 @@ When you ask Pi to run a long task, Pi Long Task:
|
|
|
26
26
|
7. Returns a summary with completed, failed, blocked, and remaining task counts, plus worker spend when available.
|
|
27
27
|
8. Optionally commits completed work after each task.
|
|
28
28
|
|
|
29
|
+
For a large project goal, this means Pi Long Task turns the single broad request into structured TODO tasks first, then assigns each TODO to a worker session one at a time. For example, a hypothetical request to build a fast team chat app would become a plan of focused tasks instead of one giant all-at-once implementation; workers would complete or report on each task incrementally before the coordinator moves to the next task.
|
|
30
|
+
|
|
29
31
|
During and after a run you get:
|
|
30
32
|
|
|
31
33
|
- a concise status summary in Pi
|
|
@@ -84,6 +86,12 @@ Run a long task with commits to implement the TODOs in @TODO.md.
|
|
|
84
86
|
Run a long task with commits to refactor the checkout flow, update the tests, and commit each completed task.
|
|
85
87
|
```
|
|
86
88
|
|
|
89
|
+
Request commits and a coverage target in the same natural-language prompt:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
Run a long task with commits with goal to have testing line coverage above 80%.
|
|
93
|
+
```
|
|
94
|
+
|
|
87
95
|
Run without commits when you want to review all changes yourself before committing:
|
|
88
96
|
|
|
89
97
|
```text
|
|
@@ -94,6 +102,100 @@ Run a long task without commits to add tests for the parser and fix any failures
|
|
|
94
102
|
Run a long task without commits to audit the README examples and leave the final diff uncommitted.
|
|
95
103
|
```
|
|
96
104
|
|
|
105
|
+
### What "with commits" means
|
|
106
|
+
|
|
107
|
+
When you ask for a long task "with commits," Pi Long Task may create a git commit after each TODO task that a worker completes with eligible changes. Each worker session is expected to stay focused on its assigned TODO only, so any commit reflects a specific slice of progress rather than the entire broad request.
|
|
108
|
+
|
|
109
|
+
Those incremental commits preserve completed work between worker sessions, make it easier to review what changed for each task, and provide clear checkpoints if a later task is blocked or needs another attempt.
|
|
110
|
+
|
|
111
|
+
### Scope expectations for broad product goals
|
|
112
|
+
|
|
113
|
+
A hypothetical request like "run a long task with commits to build a fast Slack alternative" is too large and vague to finish as one instant product build. Pi Long Task would first turn that broad goal into a realistic plan, often centered on an MVP rather than every feature of a full Slack replacement.
|
|
114
|
+
|
|
115
|
+
The generated plan would break the work into focused areas such as authentication, workspace/channel data, message creation and history, realtime sync, persistence, UI screens, tests, and deployment or configuration follow-up. Each area would become one or more TODO tasks assigned to separate worker sessions, with incremental verification and optional commits along the way.
|
|
116
|
+
|
|
117
|
+
The initial result should be expected to be a structured TODO plan, MVP-oriented breakdown, or first narrow implementation slice. A complete production-ready team chat product would require many focused tasks and repeated progress checks, not a single vague prompt completing everything immediately.
|
|
118
|
+
|
|
119
|
+
## How to run a Long Task
|
|
120
|
+
|
|
121
|
+
### 1. Prepare the work request
|
|
122
|
+
|
|
123
|
+
Pi Long Task can plan from a plain-language request or from pasted TODO markdown. If you write the TODO markdown yourself, use this structure:
|
|
124
|
+
|
|
125
|
+
```markdown
|
|
126
|
+
# Pi Long Task TODO
|
|
127
|
+
|
|
128
|
+
Global instructions:
|
|
129
|
+
|
|
130
|
+
- Keep any rule that applies to every task here.
|
|
131
|
+
|
|
132
|
+
## Progress
|
|
133
|
+
|
|
134
|
+
- [ ] TODO 1 — First focused task
|
|
135
|
+
- [ ] TODO 2 — Second focused task
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## TODO 1 — First focused task
|
|
140
|
+
|
|
141
|
+
**Goal:** Explain the outcome for this task.
|
|
142
|
+
|
|
143
|
+
**Status:**
|
|
144
|
+
|
|
145
|
+
- [ ] Implement the first focused task
|
|
146
|
+
|
|
147
|
+
**Verify:**
|
|
148
|
+
|
|
149
|
+
- Run the focused check for this task.
|
|
150
|
+
|
|
151
|
+
**Done when:**
|
|
152
|
+
|
|
153
|
+
- The task is implemented and verified.
|
|
154
|
+
|
|
155
|
+
## TODO 2 — Second focused task
|
|
156
|
+
|
|
157
|
+
**Goal:** Explain the outcome for this task.
|
|
158
|
+
|
|
159
|
+
**Status:**
|
|
160
|
+
|
|
161
|
+
- [ ] Implement the second focused task
|
|
162
|
+
|
|
163
|
+
**Verify:**
|
|
164
|
+
|
|
165
|
+
- Run the focused check for this task.
|
|
166
|
+
|
|
167
|
+
**Done when:**
|
|
168
|
+
|
|
169
|
+
- The task is implemented and verified.
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### 2. Start Pi in the target project
|
|
173
|
+
|
|
174
|
+
Install or load the extension first, then run Pi from the repository you want to modify:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
cd /path/to/your/project
|
|
178
|
+
pi
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
In the Pi prompt, use natural language:
|
|
182
|
+
|
|
183
|
+
```text
|
|
184
|
+
Run a long task without commits to implement the TODOs in @TODO.md.
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Or call the tool explicitly:
|
|
188
|
+
|
|
189
|
+
```text
|
|
190
|
+
Use pi_long_task with inputText "implement the TODOs in @TODO.md" and commit false.
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Use `with commits` or `commit true` only when you want Pi Long Task to create eligible commits after completed tasks.
|
|
194
|
+
|
|
195
|
+
### 3. Monitor progress and completion
|
|
196
|
+
|
|
197
|
+
During execution, Pi Long Task creates `tmp/pi-long-task/<run-id>/TODO.md` and `TASK_RESULT.md`, runs one isolated worker session per unfinished TODO in order, and retries unfinished tasks up to the configured attempt limit. In Pi TUI, watch the Long Task sidebar/widget for the active task, subtask checklist, task timeline, counts, and worker spend when available. In headless or non-UI runs, watch the partial tool-result updates in the main output. When the run finishes, the final response lists completed, failed, blocked, and remaining task counts plus the result and TODO file paths.
|
|
198
|
+
|
|
97
199
|
## What it looks like
|
|
98
200
|
|
|
99
201
|
In Pi TUI, Pi Long Task keeps worker activity in the main tool result flow and registers a real right-side TUI sidebar for the run timeline:
|
|
@@ -152,27 +254,141 @@ Run with commits:
|
|
|
152
254
|
Use pi_long_task with inputText "implement the TODOs in @TODO.md" and commit true.
|
|
153
255
|
```
|
|
154
256
|
|
|
257
|
+
Run with an explicit high-level goal for the planner and worker prompts:
|
|
258
|
+
|
|
259
|
+
```text
|
|
260
|
+
Use pi_long_task with inputText "update the checkout TODOs" and commit false and goal "ship a reliable checkout recovery experience".
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
When a goal is enough context, `inputText` can be omitted:
|
|
264
|
+
|
|
265
|
+
```text
|
|
266
|
+
Use pi_long_task with commit true and goal "have testing line coverage above 80%".
|
|
267
|
+
```
|
|
268
|
+
|
|
155
269
|
Use a pasted TODO plan:
|
|
156
270
|
|
|
157
271
|
```text
|
|
158
272
|
Use pi_long_task with inputText "<paste TODO markdown here>" and commit false.
|
|
159
273
|
```
|
|
160
274
|
|
|
275
|
+
## Goal-oriented iterative loop
|
|
276
|
+
|
|
277
|
+
Use `pi_goal_task` when you have a high-level outcome instead of a ready TODO plan and want Pi Long Task to keep iterating until a reviewer confirms the goal is complete.
|
|
278
|
+
|
|
279
|
+
```text
|
|
280
|
+
Use pi_goal_task with goal "modernize the settings page, add tests, and update docs" and commit true.
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Goal loops default to `commit true`, `minIterations 1`, `maxIterations 50`, a 48-hour total timeout, 3 hours per implementation iteration, and 30 minutes per reviewer pass. Override limits when you want a larger or smaller loop. When you explicitly set `maxIterations` without `minIterations`, that number also becomes the minimum target, so the loop will not stop early just because a reviewer found one pass complete.
|
|
284
|
+
|
|
285
|
+
Examples:
|
|
286
|
+
|
|
287
|
+
```text
|
|
288
|
+
Run a goal task with commits for goal: build a full Slack alternative chat app focused on speed.
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
```text
|
|
292
|
+
Use pi_goal_task with goal "build a full Slack alternative chat app focused on speed" and commit true and minIterations 100 and maxIterations 100.
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
```text
|
|
296
|
+
Use pi_goal_task with goal "ship a polished analytics dashboard with onboarding, tests, docs, and launch notes" and commit false and maxIterations 20.
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
For broad software-product goals like the Slack example, `pi_goal_task` first creates a software/product specification, then generates implementation TODOs from that spec, runs them, reviews completion, and repeats until the spec is complete and the minimum iteration target is reached, or until safety limits stop the loop.
|
|
300
|
+
|
|
301
|
+
### Discovery for vague software goals
|
|
302
|
+
|
|
303
|
+
When a `pi_goal_task` goal is vague, such as a short product direction or broad feature idea, the goal loop first runs software-focused discovery before implementation TODOs are generated. Discovery turns the original goal into a persisted product definition and definition-of-done so implementation workers do not have to guess the scope.
|
|
304
|
+
|
|
305
|
+
Discovery uses role-based planning outputs from these supported roles:
|
|
306
|
+
|
|
307
|
+
- Product Owner
|
|
308
|
+
- Project Manager
|
|
309
|
+
- Software Architect/Tech Lead
|
|
310
|
+
- UX/UI Designer
|
|
311
|
+
- QA/Reviewer
|
|
312
|
+
- Marketing/Growth, when relevant for user-facing launch or adoption context
|
|
313
|
+
|
|
314
|
+
The consolidated specification is saved as `GOAL_SPEC.json` under the goal run directory. It includes traceability to the original user goal, role-output summaries, in-scope and out-of-scope requirements, assumptions, open questions, milestones, acceptance criteria, verification gates, design constraints, product constraints, optional marketing/growth context, and a definition-of-done with required artifacts and notes.
|
|
315
|
+
|
|
316
|
+
For vague goals, the loop runs as:
|
|
317
|
+
|
|
318
|
+
1. accept the high-level `goal`
|
|
319
|
+
2. classify the goal as vague and run discovery
|
|
320
|
+
3. persist `GOAL_SPEC.json`
|
|
321
|
+
4. generate implementation TODO markdown from the persisted specification
|
|
322
|
+
5. run that generated TODO as a normal long task in an isolated worker session
|
|
323
|
+
6. run a separate reviewer session that decides `complete`, `incomplete`, `blocked`, or `failed` against the persisted specification
|
|
324
|
+
7. if the reviewer says `incomplete`, generate another TODO using previous review context plus the same persisted specification and repeat
|
|
325
|
+
|
|
326
|
+
Implementation TODO generation treats the persisted specification as the source of truth. Generated tasks are instructed to cover relevant requirement, milestone, acceptance-criterion, verification-gate, constraint, and definition-of-done items, including spec IDs such as `REQ-*`, `MS-*`, `AC-*`, and `VG-*` where applicable. Reviewer sessions also load the persisted specification and use it as the primary review target; the original goal remains available for traceability, but vague wording alone is not the completion standard.
|
|
327
|
+
|
|
328
|
+
### Concrete goals and compatibility
|
|
329
|
+
|
|
330
|
+
When a `pi_goal_task` goal is already concrete, existing direct behavior is preserved: the loop skips discovery and generates implementation TODOs from the provided goal, previous iteration context, and reviewer feedback. Goals are generally considered concrete when they already include implementation details such as files or paths, specific commands/tests, explicit acceptance criteria, or enough detailed scope for direct TODO generation.
|
|
331
|
+
|
|
332
|
+
`pi_long_task` behavior is unchanged. Discovery is only enabled by default for `pi_goal_task`; direct long-task planning, TODO normalization, worker execution, progress display, retries, artifacts, and commit behavior continue to work as before.
|
|
333
|
+
|
|
334
|
+
Goal-loop artifacts are stored under `tmp/pi-goal-task/<goal-run-id>/`, including `GOAL_STATE.json`, `GOAL_TRACE.jsonl`, `GOAL_RESULT.md`, optional `GOAL_SPEC.json` for discovered goals, and per-iteration generated TODO, worker, and reviewer files. Child TODO execution still writes normal `tmp/pi-long-task/<run-id>/` artifacts.
|
|
335
|
+
|
|
336
|
+
Safety controls:
|
|
337
|
+
|
|
338
|
+
- `minIterations` prevents early success before the requested number of loops; default is `1`.
|
|
339
|
+
- `maxIterations` stops retry loops when the reviewer keeps finding remaining work; default is `50`. If explicitly provided without `minIterations`, it is also used as the minimum target.
|
|
340
|
+
- `timeoutMs` caps the overall goal loop; default is `172800000` ms (48 hours).
|
|
341
|
+
- `iterationTimeoutMs` caps each generated TODO worker iteration; default is `10800000` ms (3 hours).
|
|
342
|
+
- `reviewerTimeoutMs` caps each reviewer session; default is `1800000` ms (30 minutes).
|
|
343
|
+
- tool cancellation is passed through and stops the loop with `cancelled` status.
|
|
344
|
+
- `maxAttemptsPerTask` and `maxBashTimeoutMs` are forwarded to worker long-task runs.
|
|
345
|
+
- `commit` controls whether implementation workers may commit; goal loops default to `commit true`, so pass `commit false` when you want to review all changes first.
|
|
346
|
+
|
|
161
347
|
## Options
|
|
162
348
|
|
|
163
|
-
|
|
349
|
+
`pi_long_task` has one required input and two optional inputs:
|
|
164
350
|
|
|
165
351
|
```ts
|
|
166
352
|
{
|
|
167
|
-
inputText: string;
|
|
168
353
|
commit: boolean;
|
|
354
|
+
inputText?: string;
|
|
355
|
+
goal?: string;
|
|
169
356
|
}
|
|
170
357
|
```
|
|
171
358
|
|
|
172
|
-
- `inputText` is the request or TODO markdown to work on.
|
|
173
359
|
- `commit` controls whether Pi Long Task may create git commits.
|
|
360
|
+
- `inputText` optionally provides the request or TODO markdown to work on.
|
|
361
|
+
- `goal` optionally provides a high-level desired outcome that is passed to TODO planning and worker task prompts. Coverage goals such as `have testing line coverage above 80%` add coverage-specific planning and verification guidance.
|
|
362
|
+
|
|
363
|
+
`pi_goal_task` accepts a high-level goal plus safety controls:
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
{
|
|
367
|
+
goal: string;
|
|
368
|
+
commit?: boolean;
|
|
369
|
+
minIterations?: number;
|
|
370
|
+
maxIterations?: number;
|
|
371
|
+
timeoutMs?: number;
|
|
372
|
+
iterationTimeoutMs?: number;
|
|
373
|
+
reviewerTimeoutMs?: number;
|
|
374
|
+
maxAttemptsPerTask?: number;
|
|
375
|
+
maxBashTimeoutMs?: number;
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Use `pi_goal_task` for iterative goal completion. Vague `pi_goal_task` goals enter discovery and persist `GOAL_SPEC.json`; already concrete goals keep the direct implementation path. Use `pi_long_task` when you already have a concrete request or TODO markdown and want one planned long-task run.
|
|
380
|
+
|
|
381
|
+
Example explicit goal-task calls:
|
|
382
|
+
|
|
383
|
+
```text
|
|
384
|
+
Use pi_goal_task with goal "build a full Slack alternative chat app focused on speed" and commit true.
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
```text
|
|
388
|
+
Use pi_goal_task with goal "build a full Slack alternative chat app focused on speed" and commit true and minIterations 100 and maxIterations 100 and timeoutMs 1296000000.
|
|
389
|
+
```
|
|
174
390
|
|
|
175
|
-
For natural-language requests, Pi Long Task routes phrases like "run a long task with commits" to the tool with commits enabled. If you ask for a long task without mentioning commits, commits stay disabled.
|
|
391
|
+
For natural-language requests, Pi Long Task routes phrases like "run a long task with commits" to the tool with commits enabled. Phrases like "with goal to have testing line coverage above 80%" are parsed into the `goal` option. If you ask for a long task without mentioning commits, commits stay disabled.
|
|
176
392
|
|
|
177
393
|
Natural-language routing intentionally avoids informational questions, such as "How do I run a long task with commits?", and explicit tool calls are left unchanged.
|
|
178
394
|
|
package/package.json
CHANGED
package/src/coordinator.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { runGuardedSessionPrompt } from "./session_guard.ts";
|
|
|
15
15
|
import { parseWorkerRuntimeConfig } from "./worker_config.ts";
|
|
16
16
|
import { buildTaskProgressModel, type TaskProgressModel, type TaskProgressStatus } from "./task_progress.ts";
|
|
17
17
|
import {
|
|
18
|
+
applyGoalInstructionsToTodoMarkdown,
|
|
18
19
|
buildTodoCreationPrompt,
|
|
19
20
|
buildTodoRepairPrompt,
|
|
20
21
|
extractAndValidateTodoMarkdown,
|
|
@@ -98,6 +99,7 @@ export interface CoordinatorProgressUpdate {
|
|
|
98
99
|
isError?: boolean;
|
|
99
100
|
totalTasks?: number;
|
|
100
101
|
workerCostTotal: number;
|
|
102
|
+
goal?: string;
|
|
101
103
|
currentTask?: CoordinatorProgressTask;
|
|
102
104
|
subtasks?: CoordinatorProgressSubtask[];
|
|
103
105
|
taskProgress?: TaskProgressModel;
|
|
@@ -142,6 +144,7 @@ export interface TodoPlannerOptions {
|
|
|
142
144
|
gracefulShutdownMs?: number;
|
|
143
145
|
sessionFactory?: WorkerSessionFactory;
|
|
144
146
|
onDiagnostic?: PlannerDiagnosticHandler;
|
|
147
|
+
goal?: string;
|
|
145
148
|
}
|
|
146
149
|
|
|
147
150
|
export interface TaskAttemptSummary {
|
|
@@ -177,6 +180,7 @@ export interface CoordinatorResult {
|
|
|
177
180
|
taskProgress: TaskProgressModel;
|
|
178
181
|
workerCostTotal: number;
|
|
179
182
|
commit: boolean;
|
|
183
|
+
goal?: string;
|
|
180
184
|
error?: string;
|
|
181
185
|
}
|
|
182
186
|
|
|
@@ -198,6 +202,7 @@ interface RuntimeOptions {
|
|
|
198
202
|
maxBashTimeoutSeconds: number;
|
|
199
203
|
workerModel?: unknown;
|
|
200
204
|
workerModelName?: string;
|
|
205
|
+
goal?: string;
|
|
201
206
|
taskThinking: string;
|
|
202
207
|
todoThinking: string;
|
|
203
208
|
todoTimeoutMs: number;
|
|
@@ -215,6 +220,7 @@ interface RuntimeOptions {
|
|
|
215
220
|
|
|
216
221
|
export async function runCoordinator(options: RunCoordinatorOptions): Promise<CoordinatorResult> {
|
|
217
222
|
const runtime = buildRuntimeOptions(options);
|
|
223
|
+
const inputText = coordinatorInputText(options);
|
|
218
224
|
const attempts: TaskAttemptSummary[] = [];
|
|
219
225
|
const outcomes: SessionOutcome[] = [];
|
|
220
226
|
const commits: CoordinatorCommitSummary[] = [];
|
|
@@ -225,7 +231,7 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
225
231
|
|
|
226
232
|
try {
|
|
227
233
|
emitProgress(runtime, "Creating TODO plan...", { phase: "planning" });
|
|
228
|
-
let todoMarkdown = await generateOrNormalizeTodoMarkdown(
|
|
234
|
+
let todoMarkdown = await generateOrNormalizeTodoMarkdown(inputText, runtime);
|
|
229
235
|
validateTodoMarkdown(todoMarkdown);
|
|
230
236
|
planningComplete = true;
|
|
231
237
|
await writeFile(runtime.todoPath, todoMarkdown, "utf8");
|
|
@@ -274,6 +280,7 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
274
280
|
commitRequested: options.commit,
|
|
275
281
|
previousAttempts: previousAttempts.get(nextTask.taskId)?.join("\n\n---\n\n"),
|
|
276
282
|
globalInstructions: todoGlobalInstructions(todoMarkdown),
|
|
283
|
+
goal: runtime.goal,
|
|
277
284
|
maxBashTimeoutSeconds: runtime.maxBashTimeoutSeconds,
|
|
278
285
|
taskTimeoutSeconds: runtime.taskTimeoutSeconds,
|
|
279
286
|
model: runtime.workerModel,
|
|
@@ -397,6 +404,7 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
397
404
|
taskProgress,
|
|
398
405
|
workerCostTotal: runtime.workerCostState.total,
|
|
399
406
|
commit: options.commit,
|
|
407
|
+
goal: runtime.goal,
|
|
400
408
|
error: failure,
|
|
401
409
|
};
|
|
402
410
|
result.message = formatCoordinatorResultMessage(result);
|
|
@@ -446,6 +454,7 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
446
454
|
taskProgress: buildTaskProgressModel({ tasks: [], attempts }),
|
|
447
455
|
workerCostTotal: runtime.workerCostState.total,
|
|
448
456
|
commit: options.commit,
|
|
457
|
+
goal: runtime.goal,
|
|
449
458
|
error: resultError,
|
|
450
459
|
};
|
|
451
460
|
result.message = formatCoordinatorResultMessage(result);
|
|
@@ -459,16 +468,17 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
459
468
|
}
|
|
460
469
|
|
|
461
470
|
async function generateOrNormalizeTodoMarkdown(inputText: string, runtime: RuntimeOptions): Promise<string> {
|
|
462
|
-
const local = todoMarkdownFromString(inputText);
|
|
471
|
+
const local = todoMarkdownFromString(inputText, runtime.goal);
|
|
463
472
|
if (local) {
|
|
464
473
|
return local;
|
|
465
474
|
}
|
|
466
475
|
|
|
467
476
|
const plannerText = await requestTodoPlan(inputText, runtime);
|
|
468
|
-
|
|
477
|
+
const planned = await extractTodoMarkdownWithOneRepair(
|
|
469
478
|
inputText,
|
|
470
479
|
plannerText,
|
|
471
480
|
(repairPrompt) => requestTodoPlan(repairPrompt, runtime),
|
|
481
|
+
runtime.goal,
|
|
472
482
|
{
|
|
473
483
|
onInvalidOutput: (validationError) =>
|
|
474
484
|
recordPlannerDiagnostic(runtime, {
|
|
@@ -487,6 +497,7 @@ async function generateOrNormalizeTodoMarkdown(inputText: string, runtime: Runti
|
|
|
487
497
|
}),
|
|
488
498
|
},
|
|
489
499
|
);
|
|
500
|
+
return applyGoalInstructionsToTodoMarkdown(planned, runtime.goal);
|
|
490
501
|
}
|
|
491
502
|
|
|
492
503
|
interface TodoExtractionRepairHooks {
|
|
@@ -499,6 +510,7 @@ async function extractTodoMarkdownWithOneRepair(
|
|
|
499
510
|
inputText: string,
|
|
500
511
|
plannerText: string,
|
|
501
512
|
requestRepair: (repairPrompt: string) => Promise<string>,
|
|
513
|
+
goal?: string,
|
|
502
514
|
hooks: TodoExtractionRepairHooks = {},
|
|
503
515
|
): Promise<string> {
|
|
504
516
|
try {
|
|
@@ -507,7 +519,7 @@ async function extractTodoMarkdownWithOneRepair(
|
|
|
507
519
|
const validationError = errorMessage(error);
|
|
508
520
|
hooks.onInvalidOutput?.(validationError);
|
|
509
521
|
hooks.onRepairAttempt?.(validationError);
|
|
510
|
-
const repairText = await requestRepair(buildTodoRepairPrompt(inputText, plannerText, validationError));
|
|
522
|
+
const repairText = await requestRepair(buildTodoRepairPrompt(inputText, plannerText, validationError, goal));
|
|
511
523
|
try {
|
|
512
524
|
return extractAndValidateTodoMarkdown(repairText);
|
|
513
525
|
} catch (repairError) {
|
|
@@ -532,6 +544,7 @@ async function requestTodoPlan(inputText: string, runtime: RuntimeOptions): Prom
|
|
|
532
544
|
gracefulShutdownMs: runtime.todoGracefulShutdownMs,
|
|
533
545
|
sessionFactory: runtime.todoSessionFactory,
|
|
534
546
|
onDiagnostic: (diagnostic) => recordPlannerDiagnostic(runtime, diagnostic),
|
|
547
|
+
goal: runtime.goal,
|
|
535
548
|
});
|
|
536
549
|
}
|
|
537
550
|
|
|
@@ -555,7 +568,7 @@ export async function runTodoPlanner(options: TodoPlannerOptions): Promise<strin
|
|
|
555
568
|
try {
|
|
556
569
|
const plannerText = await runTodoPlannerPrompt({
|
|
557
570
|
session,
|
|
558
|
-
prompt: buildTodoCreationPrompt(options.inputText),
|
|
571
|
+
prompt: buildTodoCreationPrompt(options.inputText, options.goal),
|
|
559
572
|
abortSignal: options.abortSignal,
|
|
560
573
|
timeoutMs,
|
|
561
574
|
gracefulShutdownMs,
|
|
@@ -576,6 +589,7 @@ export async function runTodoPlanner(options: TodoPlannerOptions): Promise<strin
|
|
|
576
589
|
diagnostics: result.diagnostics,
|
|
577
590
|
onDiagnostic: options.onDiagnostic,
|
|
578
591
|
}),
|
|
592
|
+
options.goal,
|
|
579
593
|
{
|
|
580
594
|
onInvalidOutput: (validationError) =>
|
|
581
595
|
options.onDiagnostic?.({
|
|
@@ -619,7 +633,7 @@ export async function runTodoPlanner(options: TodoPlannerOptions): Promise<strin
|
|
|
619
633
|
if (!plannerMarkdown) {
|
|
620
634
|
throw new TodoGenerationError("TODO planner did not return valid TODO markdown.");
|
|
621
635
|
}
|
|
622
|
-
return plannerMarkdown;
|
|
636
|
+
return applyGoalInstructionsToTodoMarkdown(plannerMarkdown, options.goal);
|
|
623
637
|
}
|
|
624
638
|
|
|
625
639
|
async function runTodoPlannerPrompt(options: {
|
|
@@ -692,7 +706,7 @@ function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
|
|
|
692
706
|
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
693
707
|
const runId = sanitizeRunId(options.runId ?? defaultRunId(options.now?.() ?? new Date()));
|
|
694
708
|
const runDir = path.join(cwd, "tmp", "pi-long-task", runId);
|
|
695
|
-
const parsedWorkerConfig = parseWorkerRuntimeConfig(options.inputText);
|
|
709
|
+
const parsedWorkerConfig = parseWorkerRuntimeConfig(options.inputText ?? "");
|
|
696
710
|
const configuredAttempts = options.maxAttemptsPerTask ?? parsedWorkerConfig.maxAttemptsPerTask;
|
|
697
711
|
const configuredTaskTimeoutMs = options.taskTimeoutMs ?? parsedWorkerConfig.taskTimeoutMs;
|
|
698
712
|
const configuredTodoTimeoutMs = options.todoTimeoutMs;
|
|
@@ -700,6 +714,7 @@ function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
|
|
|
700
714
|
const configuredMaxBashTimeoutMs = options.maxBashTimeoutMs ?? parsedWorkerConfig.maxBashTimeoutMs;
|
|
701
715
|
const workerModelName = options.workerModelName ?? parsedWorkerConfig.modelName;
|
|
702
716
|
const workerModel = workerModelName ? undefined : options.workerModel;
|
|
717
|
+
const goal = normalizeOptionalText(options.goal);
|
|
703
718
|
|
|
704
719
|
return {
|
|
705
720
|
cwd,
|
|
@@ -718,6 +733,7 @@ function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
|
|
|
718
733
|
positiveMilliseconds(configuredMaxBashTimeoutMs, DEFAULT_COORDINATOR_OPTIONS.maxBashTimeoutMs) / 1000,
|
|
719
734
|
workerModel,
|
|
720
735
|
workerModelName,
|
|
736
|
+
goal,
|
|
721
737
|
taskThinking: options.taskThinking ?? DEFAULT_COORDINATOR_OPTIONS.taskThinking,
|
|
722
738
|
todoThinking: options.todoThinking ?? DEFAULT_COORDINATOR_OPTIONS.todoThinking,
|
|
723
739
|
workerRunner: options.workerRunner ?? runWorkerTask,
|
|
@@ -744,6 +760,7 @@ function emitProgress(
|
|
|
744
760
|
resultPath: runtime.taskResultPath,
|
|
745
761
|
workerCostTotal: runtime.workerCostState.total,
|
|
746
762
|
...update,
|
|
763
|
+
goal: runtime.goal,
|
|
747
764
|
});
|
|
748
765
|
}
|
|
749
766
|
|
|
@@ -1153,6 +1170,15 @@ function sanitizeRunId(runId: string): string {
|
|
|
1153
1170
|
return sanitized || defaultRunId(new Date());
|
|
1154
1171
|
}
|
|
1155
1172
|
|
|
1173
|
+
function normalizeOptionalText(value: string | undefined): string | undefined {
|
|
1174
|
+
const trimmed = value?.trim();
|
|
1175
|
+
return trimmed ? trimmed : undefined;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function coordinatorInputText(options: RunCoordinatorOptions): string {
|
|
1179
|
+
return normalizeOptionalText(options.inputText) ?? normalizeOptionalText(options.goal) ?? "";
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1156
1182
|
function positiveInteger(value: number | undefined, fallback: number): number {
|
|
1157
1183
|
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
1158
1184
|
return Math.floor(value);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export interface CoverageGoal {
|
|
2
|
+
thresholdPercent: number;
|
|
3
|
+
thresholdText: string;
|
|
4
|
+
relation: "above" | "at least";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const PERCENT_RE = /(\d+(?:\.\d+)?)\s*%/;
|
|
8
|
+
const COVERAGE_RE = /\b(?:test(?:ing)?\s+)?(?:line\s+)?coverage\b/i;
|
|
9
|
+
const ABOVE_RE = /(?:\babove\b|\bover\b|\bgreater\s+than\b|\bmore\s+than\b|>)/i;
|
|
10
|
+
const AT_LEAST_RE = /(?:\bat\s+least\b|\bminimum\b|\bmin\b|\bno\s+less\s+than\b|>=)/i;
|
|
11
|
+
|
|
12
|
+
export function parseCoverageGoal(text: string | undefined): CoverageGoal | undefined {
|
|
13
|
+
const trimmed = text?.trim();
|
|
14
|
+
if (!trimmed || !COVERAGE_RE.test(trimmed)) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const percent = PERCENT_RE.exec(trimmed);
|
|
19
|
+
if (!percent?.[1]) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const thresholdPercent = Number.parseFloat(percent[1]);
|
|
24
|
+
if (!Number.isFinite(thresholdPercent) || thresholdPercent < 0 || thresholdPercent > 100) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
thresholdPercent,
|
|
30
|
+
thresholdText: formatPercent(thresholdPercent),
|
|
31
|
+
relation: coverageRelation(trimmed),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function coverageGoalPhrase(goal: CoverageGoal): string {
|
|
36
|
+
return `testing line coverage ${goal.relation} ${goal.thresholdText}%`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function coverageGoalAction(goal: CoverageGoal): string {
|
|
40
|
+
return `Raise or maintain ${coverageGoalPhrase(goal)}.`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function coverageGoalVerification(goal: CoverageGoal): string {
|
|
44
|
+
return `Run the repository's coverage command and confirm line coverage is ${goal.relation} ${goal.thresholdText}%; report the command and resulting line coverage.`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function coverageGoalVerifyBullet(goal: CoverageGoal): string {
|
|
48
|
+
return `- ${coverageGoalVerification(goal)}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function inferCoverageGoalText(text: string): string | undefined {
|
|
52
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
53
|
+
if (!parseCoverageGoal(normalized)) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const coverageIndex = normalized.search(/(?:test(?:ing)?\s+)?(?:line\s+)?coverage/i);
|
|
58
|
+
if (coverageIndex < 0) {
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const prefix = normalized.slice(0, coverageIndex).trimEnd();
|
|
63
|
+
const action = /(have|reach|raise|maintain|keep|increase|get|achieve|hit|ensure)\s*$/i.exec(prefix);
|
|
64
|
+
const start = action?.index ?? coverageIndex;
|
|
65
|
+
return normalizeCoverageGoalText(normalized.slice(start));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeCoverageGoalText(text: string): string | undefined {
|
|
69
|
+
const trimmed = text
|
|
70
|
+
.replace(/^\b(?:to|for|that)\b\s+/i, "")
|
|
71
|
+
.replace(/[.,;:!?]+$/g, "")
|
|
72
|
+
.trim();
|
|
73
|
+
return trimmed || undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function coverageRelation(text: string): CoverageGoal["relation"] {
|
|
77
|
+
const percentIndex = text.search(PERCENT_RE);
|
|
78
|
+
const relationWindow = percentIndex >= 0 ? text.slice(Math.max(0, percentIndex - 40), percentIndex + 8) : text;
|
|
79
|
+
if (AT_LEAST_RE.test(relationWindow)) {
|
|
80
|
+
return "at least";
|
|
81
|
+
}
|
|
82
|
+
if (ABOVE_RE.test(relationWindow)) {
|
|
83
|
+
return "above";
|
|
84
|
+
}
|
|
85
|
+
return "at least";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function formatPercent(value: number): string {
|
|
89
|
+
return Number.isInteger(value) ? String(value) : String(value).replace(/0+$/g, "").replace(/\.$/, "");
|
|
90
|
+
}
|