pi-long-task 0.3.9 → 0.3.10

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 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. 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.
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,139 @@ 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`, `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.
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 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 or 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
+ - `maxIterations` stops retry loops when the reviewer keeps finding remaining work; default is `50`.
339
+ - `timeoutMs` caps the overall goal loop; default is `172800000` ms (48 hours).
340
+ - `iterationTimeoutMs` caps each generated TODO worker iteration; default is `10800000` ms (3 hours).
341
+ - `reviewerTimeoutMs` caps each reviewer session; default is `1800000` ms (30 minutes).
342
+ - tool cancellation is passed through and stops the loop with `cancelled` status.
343
+ - `maxAttemptsPerTask` and `maxBashTimeoutMs` are forwarded to worker long-task runs.
344
+ - `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.
345
+
161
346
  ## Options
162
347
 
163
- The tool has two inputs:
348
+ `pi_long_task` has one required input and two optional inputs:
164
349
 
165
350
  ```ts
166
351
  {
167
- inputText: string;
168
352
  commit: boolean;
353
+ inputText?: string;
354
+ goal?: string;
169
355
  }
170
356
  ```
171
357
 
172
- - `inputText` is the request or TODO markdown to work on.
173
358
  - `commit` controls whether Pi Long Task may create git commits.
359
+ - `inputText` optionally provides the request or TODO markdown to work on.
360
+ - `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.
361
+
362
+ `pi_goal_task` accepts a high-level goal plus safety controls:
363
+
364
+ ```ts
365
+ {
366
+ goal: string;
367
+ commit?: boolean;
368
+ maxIterations?: number;
369
+ timeoutMs?: number;
370
+ iterationTimeoutMs?: number;
371
+ reviewerTimeoutMs?: number;
372
+ maxAttemptsPerTask?: number;
373
+ maxBashTimeoutMs?: number;
374
+ }
375
+ ```
376
+
377
+ 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.
378
+
379
+ Example explicit goal-task calls:
380
+
381
+ ```text
382
+ Use pi_goal_task with goal "build a full Slack alternative chat app focused on speed" and commit true.
383
+ ```
384
+
385
+ ```text
386
+ Use pi_goal_task with goal "build a full Slack alternative chat app focused on speed" and commit true and maxIterations 100 and timeoutMs 172800000.
387
+ ```
174
388
 
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.
389
+ 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
390
 
177
391
  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
392
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-long-task",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "type": "module",
5
5
  "description": "Pi extension for breaking down and running long coding tasks safely.",
6
6
  "keywords": [
@@ -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(options.inputText, runtime);
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
- return extractTodoMarkdownWithOneRepair(
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
+ }