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.
@@ -1,3 +1,5 @@
1
+ import { inferCoverageGoalText } from "./coverage_goal.ts";
2
+
1
3
  const LONG_TASK_RE = /\b(?:long[-\s]?task|longtask|large[-\s]?task|big[-\s]?task|multi[-\s]?step(?:\s+task)?)\b/i;
2
4
  const DIRECT_LONG_TASK_REQUEST_RE =
3
5
  /\b(?:run|start|do|handle|execute|launch|kick\s+off|use)\s+(?:a\s+|the\s+)?(?:long[-\s]?task|longtask|large[-\s]?task|big[-\s]?task|multi[-\s]?step(?:\s+task)?)\b/i;
@@ -6,20 +8,69 @@ const WANT_LONG_TASK_RE =
6
8
  const NEGATED_LONG_TASK_RE =
7
9
  /\b(?:do\s+not|don't|dont|never)\s+(?:run|start|do|handle|execute|launch|kick\s+off|use)\s+(?:a\s+|the\s+)?(?:long[-\s]?task|longtask|large[-\s]?task|big[-\s]?task|multi[-\s]?step(?:\s+task)?)\b/i;
8
10
  const INFORMATION_QUESTION_RE = /^\s*(?:how|what|why|when|where|who)\b/i;
9
- const EXPLICIT_TOOL_RE = /\bpi_long_task\b/i;
11
+ const EXPLICIT_TOOL_RE = /\b(?:pi_long_task|pi_goal_task)\b/i;
12
+ const GOAL_LOOP_RE =
13
+ /\b(?:goal[-\s]?oriented\s+(?:loop|long[-\s]?task|task)|goal\s+(?:loop|task)|iterative\s+long[-\s]?task|repeat\s+until\s+complete|iterate\s+until\s+complete)\b/i;
14
+ const DIRECT_GOAL_TASK_REQUEST_RE =
15
+ /\b(?:run|start|do|handle|execute|launch|kick\s+off|use)\s+(?:a\s+|the\s+)?(?:goal[-\s]?oriented\s+(?:loop|long[-\s]?task|task)|goal\s+(?:loop|task)|iterative\s+long[-\s]?task)\b/i;
10
16
 
11
17
  const COMMIT_FALSE_RE =
12
18
  /\b(?:without|no|disable|disabled|off)\s+commits?\b|\bcommits?\s*(?:false|off|disabled)\b|\bcommit\s*:\s*(?:false|off|no)\b|\b(?:do\s+not|don't|dont)\s+commit\b/i;
13
19
  const COMMIT_TRUE_RE =
14
20
  /\bwith\s+commits?\b|\bcommits?\s*(?:true|on|enabled)\b|\bcommit\s*:\s*(?:true|on|yes)\b|\bcommit(?:ting)?\s+as\s+(?:you|we)\s+go\b|\b(?:make|create|include|allow|enable)\s+commits?\b/i;
21
+ const GOAL_RE = /\b(?:with\s+(?:the\s+)?goal|goal)\s*(?::|=|\b(?:to|of|for|that)\b)\s*([\s\S]+)$/i;
22
+ const TRAILING_COMMIT_MODIFIER_RE =
23
+ /(?:\s+(?:with|without|no|enable|enabled|disable|disabled)\s+commits?|\s+commits?\s*(?:true|false|on|off|enabled|disabled)|\s+commit\s*:\s*(?:true|false|on|off|yes|no))\s*$/i;
24
+ const TRAILING_ITERATION_MODIFIER_RE =
25
+ /(?:[.;,\s]+(?:(?:use|set)?\s*(?:min(?:imum)?|max(?:imum)?)\s+(?:iterations?|loops?|passes?|cycles?)\s*(?::|=|of|to)?\s*\d+|at\s+least\s+\d+\s*(?:iterations?|loops?|passes?|cycles?)|(?:i\s+want\s+it\s+to\s+)?run\s+(?:for|in)\s*\d+\s*(?:iterations?|loops?|passes?|cycles?)))[.!?]*\s*$/i;
26
+
27
+ export interface ParsedLongTaskRequestOptions {
28
+ commit: boolean;
29
+ goal?: string;
30
+ }
31
+
32
+ export interface ParsedGoalTaskRequestOptions extends ParsedLongTaskRequestOptions {
33
+ minIterations?: number;
34
+ maxIterations?: number;
35
+ }
15
36
 
16
37
  export function longTaskInputTransform(text: string): string | undefined {
38
+ const goalLoopPrompt = goalTaskInputTransform(text);
39
+ if (goalLoopPrompt) {
40
+ return goalLoopPrompt;
41
+ }
42
+
17
43
  if (!isNaturalLanguageLongTaskRequest(text)) {
18
44
  return undefined;
19
45
  }
20
46
 
21
- const commit = inferCommitSetting(text) ?? false;
22
- return buildLongTaskToolPrompt(text, commit);
47
+ return buildLongTaskToolPrompt(text, parseLongTaskRequestOptions(text));
48
+ }
49
+
50
+ export function goalTaskInputTransform(text: string): string | undefined {
51
+ if (!isNaturalLanguageGoalTaskRequest(text)) {
52
+ return undefined;
53
+ }
54
+
55
+ return buildGoalTaskToolPrompt(text, parseGoalTaskRequestOptions(text));
56
+ }
57
+
58
+ export function parseLongTaskRequestOptions(text: string): ParsedLongTaskRequestOptions {
59
+ return {
60
+ commit: inferCommitSetting(text) ?? false,
61
+ goal: inferGoalSetting(text),
62
+ };
63
+ }
64
+
65
+ export function parseGoalTaskRequestOptions(text: string): ParsedGoalTaskRequestOptions {
66
+ const maxIterations = inferMaxIterations(text);
67
+ const minIterations = inferMinIterations(text) ?? inferExactIterationCount(text) ?? maxIterations;
68
+ return {
69
+ commit: inferCommitSetting(text) ?? true,
70
+ goal: inferGoalSetting(text) ?? inferGoalLoopText(text),
71
+ minIterations,
72
+ maxIterations,
73
+ };
23
74
  }
24
75
 
25
76
  export function isNaturalLanguageLongTaskRequest(text: string): boolean {
@@ -33,6 +84,22 @@ export function isNaturalLanguageLongTaskRequest(text: string): boolean {
33
84
  return DIRECT_LONG_TASK_REQUEST_RE.test(trimmed) || WANT_LONG_TASK_RE.test(trimmed);
34
85
  }
35
86
 
87
+ export function isNaturalLanguageGoalTaskRequest(text: string): boolean {
88
+ const trimmed = text.trim();
89
+ if (!trimmed || trimmed.startsWith("/") || EXPLICIT_TOOL_RE.test(trimmed)) {
90
+ return false;
91
+ }
92
+ if (INFORMATION_QUESTION_RE.test(trimmed) || NEGATED_LONG_TASK_RE.test(trimmed)) {
93
+ return false;
94
+ }
95
+ return (
96
+ GOAL_LOOP_RE.test(trimmed) &&
97
+ (DIRECT_GOAL_TASK_REQUEST_RE.test(trimmed) ||
98
+ DIRECT_LONG_TASK_REQUEST_RE.test(trimmed) ||
99
+ WANT_LONG_TASK_RE.test(trimmed))
100
+ );
101
+ }
102
+
36
103
  export function inferCommitSetting(text: string): boolean | undefined {
37
104
  if (COMMIT_FALSE_RE.test(text)) {
38
105
  return false;
@@ -43,11 +110,91 @@ export function inferCommitSetting(text: string): boolean | undefined {
43
110
  return undefined;
44
111
  }
45
112
 
46
- function buildLongTaskToolPrompt(originalText: string, commit: boolean): string {
113
+ export function inferGoalSetting(text: string): string | undefined {
114
+ const match = GOAL_RE.exec(text);
115
+ if (!match?.[1]) {
116
+ return inferCoverageGoalText(text);
117
+ }
118
+ return normalizeGoalText(match[1]);
119
+ }
120
+
121
+ function normalizeGoalText(text: string): string | undefined {
122
+ let goal = text.trim();
123
+ while (TRAILING_COMMIT_MODIFIER_RE.test(goal) || TRAILING_ITERATION_MODIFIER_RE.test(goal)) {
124
+ goal = goal.replace(TRAILING_COMMIT_MODIFIER_RE, "").replace(TRAILING_ITERATION_MODIFIER_RE, "").trim();
125
+ }
126
+ goal = goal.replace(/^["'“”‘’]+|["'“”‘’.,;:!?]+$/g, "").trim();
127
+ return goal || undefined;
128
+ }
129
+
130
+ function inferMaxIterations(text: string): number | undefined {
131
+ const match = /\bmax(?:imum)?\s+(?:iterations?|loops?|passes?|cycles?)\s*(?::|=|of|to)?\s*(\d+)\b/i.exec(text);
132
+ return positiveIntegerMatch(match?.[1]);
133
+ }
134
+
135
+ function inferMinIterations(text: string): number | undefined {
136
+ const explicitMin = /\bmin(?:imum)?\s+(?:iterations?|loops?|passes?|cycles?)\s*(?::|=|of|to)?\s*(\d+)\b/i.exec(text);
137
+ if (explicitMin?.[1]) {
138
+ return positiveIntegerMatch(explicitMin[1]);
139
+ }
140
+ const atLeast = /\bat\s+least\s+(\d+)\s*(?:iterations?|loops?|passes?|cycles?)\b/i.exec(text);
141
+ return positiveIntegerMatch(atLeast?.[1]);
142
+ }
143
+
144
+ function inferExactIterationCount(text: string): number | undefined {
145
+ const match =
146
+ /\b(?:run|iterate|loop|cycle|spin)(?:\s+\w+){0,6}?\s+(?:for|in)?\s*(\d+)\s*(?:iterations?|loops?|passes?|cycles?)\b/i.exec(
147
+ text,
148
+ );
149
+ return positiveIntegerMatch(match?.[1]);
150
+ }
151
+
152
+ function positiveIntegerMatch(value: string | undefined): number | undefined {
153
+ const parsed = value ? Number.parseInt(value, 10) : undefined;
154
+ return parsed && Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
155
+ }
156
+
157
+ function inferGoalLoopText(text: string): string | undefined {
158
+ let goal = text.trim();
159
+ goal = goal.replace(
160
+ /^\s*(?:please\s+)?(?:run|start|do|handle|execute|launch|kick\s+off|use)\s+(?:a\s+|the\s+)?/i,
161
+ "",
162
+ );
163
+ goal = goal.replace(GOAL_LOOP_RE, "").trim();
164
+ goal = goal.replace(/^\s*(?:with\s+)?(?:goal|to|for|that)\b\s*/i, "");
165
+ return normalizeGoalText(goal);
166
+ }
167
+
168
+ function buildLongTaskToolPrompt(originalText: string, options: ParsedLongTaskRequestOptions): string {
169
+ const goalLine = options.goal ? [`Set goal to ${JSON.stringify(options.goal)}.`] : [];
47
170
  return [
48
171
  "Use the pi_long_task tool for this request.",
49
- `Set commit to ${commit ? "true" : "false"}.`,
50
- "Set inputText to the user's original request below. Do not perform the work directly outside pi_long_task.",
172
+ `Set commit to ${options.commit ? "true" : "false"}.`,
173
+ ...goalLine,
174
+ "Do not rely on inputText for parsed options; commit and goal are parsed separately. Use the original request as inputText only when the tool call includes inputText.",
175
+ "Do not perform the work directly outside pi_long_task.",
176
+ "",
177
+ "Original request:",
178
+ "```text",
179
+ originalText.trim(),
180
+ "```",
181
+ ].join("\n");
182
+ }
183
+
184
+ function buildGoalTaskToolPrompt(originalText: string, options: ParsedGoalTaskRequestOptions): string {
185
+ const goalLine = options.goal
186
+ ? `Set goal to ${JSON.stringify(options.goal)}.`
187
+ : "Set goal from the original request.";
188
+ const minIterationsLine = options.minIterations ? [`Set minIterations to ${options.minIterations}.`] : [];
189
+ const maxIterationsLine = options.maxIterations ? [`Set maxIterations to ${options.maxIterations}.`] : [];
190
+ return [
191
+ "Use the pi_goal_task tool for this request.",
192
+ goalLine,
193
+ `Set commit to ${options.commit ? "true" : "false"}.`,
194
+ ...minIterationsLine,
195
+ ...maxIterationsLine,
196
+ "The goal loop will generate TODO markdown, execute it as a long task, review completion, and repeat until complete or stopped by limits.",
197
+ "Do not perform the work directly outside pi_goal_task.",
51
198
  "",
52
199
  "Original request:",
53
200
  "```text",
package/src/render.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { AgentToolResult, Theme, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
2
2
  import { Text, type Component } from "@earendil-works/pi-tui";
3
3
 
4
+ import type { GoalLoopState, GoalLoopStatus } from "./goal_loop.ts";
4
5
  import type { TaskProgressModel } from "./task_progress.ts";
5
6
  import type { CoordinatorCommitSummary, CoordinatorRemainingTask, CoordinatorStatus } from "./types.ts";
6
7
 
@@ -18,6 +19,7 @@ export interface CoordinatorResultForRendering {
18
19
  remainingTasks?: CoordinatorRemainingTask[];
19
20
  taskProgress?: TaskProgressModel;
20
21
  workerCostTotal?: number;
22
+ goal?: string;
21
23
  error?: string;
22
24
  }
23
25
 
@@ -80,11 +82,16 @@ export function formatCoordinatorResultMessage(result: CoordinatorResultForRende
80
82
  return lines.join("\n");
81
83
  }
82
84
 
83
- export function renderLongTaskToolCall(args: { inputText?: string; commit?: boolean }, theme: Theme): Text {
85
+ export function renderLongTaskToolCall(
86
+ args: { inputText?: string; commit?: boolean; goal?: string },
87
+ theme: Theme,
88
+ ): Text {
84
89
  const commit = args.commit ? theme.fg("warning", "commit:on") : theme.fg("dim", "commit:off");
85
90
  const input = oneLine(args.inputText ?? "");
91
+ const goal = oneLine(args.goal ?? "");
92
+ const goalPreview = goal ? ` ${theme.fg("muted", `goal:${quote(truncatePlain(goal, 48))}`)}` : "";
86
93
  const preview = input ? ` ${theme.fg("muted", quote(truncatePlain(input, 96)))}` : "";
87
- return new Text(`${theme.fg("toolTitle", theme.bold("pi_long_task"))} ${commit}${preview}`, 0, 0);
94
+ return new Text(`${theme.fg("toolTitle", theme.bold("pi_long_task"))} ${commit}${goalPreview}${preview}`, 0, 0);
88
95
  }
89
96
 
90
97
  export function renderLongTaskToolResult(
@@ -105,6 +112,218 @@ export function renderLongTaskToolResult(
105
112
  return new Text(renderLongTaskSummary(finalDetails, options.expanded, theme), 0, 0);
106
113
  }
107
114
 
115
+ export interface GoalLoopResultForRendering {
116
+ state: GoalLoopState;
117
+ resultPath: string;
118
+ workerCostTotal: number;
119
+ reviewerCostTotal: number;
120
+ totalCost: number;
121
+ }
122
+
123
+ export interface GoalTaskToolRenderDetails {
124
+ goalRunId: string;
125
+ goal: string;
126
+ status: GoalLoopStatus;
127
+ currentIteration: number;
128
+ totalIterations: number;
129
+ minIterations: number;
130
+ maxIterations: number;
131
+ resultPath: string;
132
+ statePath?: string;
133
+ tracePath?: string;
134
+ workerCostTotal?: number;
135
+ reviewerCostTotal?: number;
136
+ totalCost?: number;
137
+ completionReason?: string;
138
+ latestReviewerDecision?: string;
139
+ remainingWork?: string[];
140
+ error?: string;
141
+ }
142
+
143
+ export function formatGoalLoopResultMessage(result: GoalLoopResultForRendering): string {
144
+ const state = result.state;
145
+ const lines = [
146
+ `Pi Goal Task: ${state.status}`,
147
+ `Goal: ${state.goal}`,
148
+ `Iterations: ${state.iterations.length}/${state.limits.maxIterations}`,
149
+ `Minimum iterations: ${state.limits.minIterations}`,
150
+ `Result file: ${result.resultPath}`,
151
+ `State file: ${state.goalRunDir}/GOAL_STATE.json`,
152
+ ];
153
+ if (state.completion?.reason) {
154
+ lines.push(`Outcome: ${state.completion.reason}`);
155
+ }
156
+ if (result.totalCost > 0) {
157
+ lines.push(`Worker/reviewer spend: ${formatCost(result.totalCost)}`);
158
+ }
159
+ const latestReview = [...state.iterations].reverse().find((iteration) => iteration.reviewerResult)?.reviewerResult;
160
+ if (latestReview) {
161
+ lines.push(`Latest review: ${latestReview.decision} — ${latestReview.summary}`);
162
+ if (latestReview.remainingWork.length > 0) {
163
+ lines.push("Remaining work:", ...latestReview.remainingWork.map((item) => `- ${item}`));
164
+ }
165
+ }
166
+ return lines.join("\n");
167
+ }
168
+
169
+ export function goalTaskDetailsFromResult(result: GoalLoopResultForRendering): GoalTaskToolRenderDetails {
170
+ const latestReview = [...result.state.iterations]
171
+ .reverse()
172
+ .find((iteration) => iteration.reviewerResult)?.reviewerResult;
173
+ return {
174
+ goalRunId: result.state.goalRunId,
175
+ goal: result.state.goal,
176
+ status: result.state.status,
177
+ currentIteration: result.state.currentIteration,
178
+ totalIterations: result.state.iterations.length,
179
+ minIterations: result.state.limits.minIterations,
180
+ maxIterations: result.state.limits.maxIterations,
181
+ resultPath: result.resultPath,
182
+ statePath: `${result.state.goalRunDir}/GOAL_STATE.json`,
183
+ tracePath: `${result.state.goalRunDir}/GOAL_TRACE.jsonl`,
184
+ workerCostTotal: result.workerCostTotal,
185
+ reviewerCostTotal: result.reviewerCostTotal,
186
+ totalCost: result.totalCost,
187
+ completionReason: result.state.completion?.reason,
188
+ latestReviewerDecision: latestReview?.decision,
189
+ remainingWork: latestReview?.remainingWork,
190
+ };
191
+ }
192
+
193
+ export function renderGoalTaskToolCall(
194
+ args: {
195
+ goal?: string;
196
+ commit?: boolean;
197
+ minIterations?: number;
198
+ maxIterations?: number;
199
+ timeoutMs?: number;
200
+ reviewerTimeoutMs?: number;
201
+ },
202
+ theme: Theme,
203
+ ): Text {
204
+ const commit = (args.commit ?? true) ? theme.fg("warning", "commit:on") : theme.fg("dim", "commit:off");
205
+ const goal = oneLine(args.goal ?? "");
206
+ const limits = [
207
+ args.minIterations ? `min:${args.minIterations}` : undefined,
208
+ args.maxIterations ? `max:${args.maxIterations}` : undefined,
209
+ args.timeoutMs ? `timeout:${args.timeoutMs}ms` : undefined,
210
+ args.reviewerTimeoutMs ? `review:${args.reviewerTimeoutMs}ms` : undefined,
211
+ ]
212
+ .filter(Boolean)
213
+ .join(" ");
214
+ const limitPreview = limits ? ` ${theme.fg("muted", limits)}` : "";
215
+ const goalPreview = goal ? ` ${theme.fg("muted", quote(truncatePlain(goal, 80)))}` : "";
216
+ return new Text(`${theme.fg("toolTitle", theme.bold("pi_goal_task"))} ${commit}${limitPreview}${goalPreview}`, 0, 0);
217
+ }
218
+
219
+ export function renderGoalTaskToolResult(
220
+ result: AgentToolResult<unknown>,
221
+ options: ToolRenderResultOptions,
222
+ theme: Theme,
223
+ ): Component {
224
+ const details = recordOrUndefined(result.details);
225
+ if (options.isPartial) {
226
+ return new Text(renderGoalTaskProgress(details, contentText(result), theme), 0, 0);
227
+ }
228
+
229
+ const finalDetails = goalTaskDetails(details);
230
+ if (!finalDetails) {
231
+ return new Text(contentText(result), 0, 0);
232
+ }
233
+
234
+ return new Text(renderGoalTaskSummary(finalDetails, options.expanded, theme), 0, 0);
235
+ }
236
+
237
+ function renderGoalTaskProgress(details: Record<string, unknown> | undefined, fallback: string, theme: Theme): string {
238
+ const message = stringValue(details?.message) || firstLine(fallback) || "Pi Goal Task is running...";
239
+ const phase = stringValue(details?.phase);
240
+ const iteration = numberValue(details?.iteration) ?? numberValue(details?.currentIteration);
241
+ const minIterations = numberValue(details?.minIterations);
242
+ const maxIterations = numberValue(details?.maxIterations);
243
+ const status = stringValue(details?.status) || "running";
244
+ const cost = numberValue(details?.totalCost);
245
+ const meta = [
246
+ iteration ? `iteration ${iteration}${maxIterations ? `/${maxIterations}` : ""}` : undefined,
247
+ minIterations && iteration && iteration < minIterations ? `min:${minIterations}` : undefined,
248
+ status,
249
+ cost && cost > 0 ? formatCost(cost) : undefined,
250
+ ]
251
+ .filter(Boolean)
252
+ .join(` ${theme.fg("dim", "·")} `);
253
+ const icon = phase === "complete" ? (status === "done" ? "✓" : "!") : "+";
254
+ const color = phase === "complete" ? statusColor(status) : "warning";
255
+ return `${theme.fg(color, icon)} ${theme.fg(color, "Pi Goal Task")}${meta ? ` ${theme.fg("dim", "·")} ${theme.fg("muted", meta)}` : ""}\n ${theme.fg("dim", message)}`;
256
+ }
257
+
258
+ function renderGoalTaskSummary(details: GoalTaskToolRenderDetails, expanded: boolean, theme: Theme): string {
259
+ const statusStyle = statusColor(details.status);
260
+ const icon =
261
+ details.status === "done" ? "✓" : details.status === "failed" ? "✗" : details.status === "cancelled" ? "×" : "!";
262
+ const summary = [
263
+ `${theme.fg(statusStyle, icon)} ${theme.fg("toolTitle", theme.bold("Pi Goal Task"))} ${theme.fg(statusStyle, details.status)}`,
264
+ theme.fg("muted", `${details.totalIterations}/${details.maxIterations} iterations`),
265
+ details.totalIterations < details.minIterations ? theme.fg("muted", `min:${details.minIterations}`) : undefined,
266
+ details.latestReviewerDecision ? theme.fg("muted", `review:${details.latestReviewerDecision}`) : undefined,
267
+ details.totalCost ? theme.fg("muted", `spend ${formatCost(details.totalCost)}`) : undefined,
268
+ ].filter(Boolean);
269
+
270
+ if (!expanded) {
271
+ return summary.join(" — ");
272
+ }
273
+
274
+ const lines = [summary.join(" — "), theme.fg("muted", details.goal)];
275
+ if (details.completionReason) {
276
+ lines.push(theme.fg("dim", `Outcome: ${details.completionReason}`));
277
+ }
278
+ lines.push(theme.fg("dim", `Result: ${details.resultPath}`));
279
+ if (details.statePath) {
280
+ lines.push(theme.fg("dim", `State: ${details.statePath}`));
281
+ }
282
+ if (details.tracePath) {
283
+ lines.push(theme.fg("dim", `Trace: ${details.tracePath}`));
284
+ }
285
+ const remaining = details.remainingWork ?? [];
286
+ if (remaining.length > 0) {
287
+ lines.push(theme.fg("muted", "Remaining work:"), ...remaining.map((item) => theme.fg("dim", `- ${item}`)));
288
+ }
289
+ if (details.error) {
290
+ lines.push(theme.fg("error", `Error: ${details.error}`));
291
+ }
292
+ return lines.join("\n");
293
+ }
294
+
295
+ function goalTaskDetails(details: Record<string, unknown> | undefined): GoalTaskToolRenderDetails | undefined {
296
+ if (!details) {
297
+ return undefined;
298
+ }
299
+ const goalRunId = stringValue(details.goalRunId);
300
+ const goal = stringValue(details.goal);
301
+ const status = goalLoopStatus(details.status);
302
+ const resultPath = stringValue(details.resultPath);
303
+ if (!goalRunId || !goal || !status || !resultPath) {
304
+ return undefined;
305
+ }
306
+ return {
307
+ goalRunId,
308
+ goal,
309
+ status,
310
+ currentIteration: numberValue(details.currentIteration) ?? 0,
311
+ totalIterations: numberValue(details.totalIterations) ?? 0,
312
+ minIterations: numberValue(details.minIterations) ?? 0,
313
+ maxIterations: numberValue(details.maxIterations) ?? 0,
314
+ resultPath,
315
+ statePath: stringValue(details.statePath),
316
+ tracePath: stringValue(details.tracePath),
317
+ workerCostTotal: numberValue(details.workerCostTotal),
318
+ reviewerCostTotal: numberValue(details.reviewerCostTotal),
319
+ totalCost: numberValue(details.totalCost),
320
+ completionReason: stringValue(details.completionReason),
321
+ latestReviewerDecision: stringValue(details.latestReviewerDecision),
322
+ remainingWork: stringArray(details.remainingWork),
323
+ error: stringValue(details.error),
324
+ };
325
+ }
326
+
108
327
  function renderLongTaskProgress(details: Record<string, unknown> | undefined, fallback: string, theme: Theme): string {
109
328
  const message = stringValue(details?.message) || firstLine(fallback) || "Pi Long Task is running...";
110
329
  const phase = stringValue(details?.phase);
@@ -422,11 +641,11 @@ function remainingTaskSummaries(value: unknown): CoordinatorRemainingTask[] {
422
641
  });
423
642
  }
424
643
 
425
- function statusColor(status: CoordinatorStatus): "success" | "warning" | "error" {
644
+ function statusColor(status: CoordinatorStatus | GoalLoopStatus | string): "success" | "warning" | "error" {
426
645
  if (status === "done") {
427
646
  return "success";
428
647
  }
429
- if (status === "failed") {
648
+ if (status === "failed" || status === "cancelled") {
430
649
  return "error";
431
650
  }
432
651
  return "warning";
@@ -436,6 +655,17 @@ function isCoordinatorStatus(value: string): value is CoordinatorStatus {
436
655
  return value === "done" || value === "partial" || value === "blocked" || value === "failed";
437
656
  }
438
657
 
658
+ function goalLoopStatus(value: unknown): GoalLoopStatus | undefined {
659
+ return value === "running" ||
660
+ value === "done" ||
661
+ value === "partial" ||
662
+ value === "blocked" ||
663
+ value === "failed" ||
664
+ value === "cancelled"
665
+ ? value
666
+ : undefined;
667
+ }
668
+
439
669
  function contentText(result: AgentToolResult<unknown>): string {
440
670
  const content = Array.isArray(result.content) ? result.content : [];
441
671
  return content
@@ -475,6 +705,10 @@ function nonNegativeNumberValue(value: unknown): number | undefined {
475
705
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
476
706
  }
477
707
 
708
+ function stringArray(value: unknown): string[] {
709
+ return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
710
+ }
711
+
478
712
  function formatCost(value: number): string {
479
713
  if (value === 0) {
480
714
  return "$0";
@@ -1,3 +1,9 @@
1
+ import {
2
+ coverageGoalAction,
3
+ coverageGoalVerification,
4
+ coverageGoalVerifyBullet,
5
+ parseCoverageGoal,
6
+ } from "./coverage_goal.ts";
1
7
  import { parseTasks, TodoParseError } from "./todo_parser.ts";
2
8
 
3
9
  const TODO_HEADING_RE = /^##\s+TODO\s+(\d+)\s+[—-]\s+(.+?)\s*$/gm;
@@ -20,21 +26,21 @@ interface ExistingTask {
20
26
  body: string;
21
27
  }
22
28
 
23
- export function todoMarkdownFromString(rawInput: string): string | undefined {
29
+ export function todoMarkdownFromString(rawInput: string, goal?: string): string | undefined {
24
30
  const input = rawInput.trim();
25
31
  if (!input) {
26
32
  return undefined;
27
33
  }
28
34
 
29
35
  if (hasTodoHeadings(input)) {
30
- const markdown = normalizeExistingTodoMarkdown(input);
36
+ const markdown = applyGoalInstructionsToTodoMarkdown(normalizeExistingTodoMarkdown(input), goal);
31
37
  validateTodoMarkdown(markdown);
32
38
  return markdown;
33
39
  }
34
40
 
35
41
  const listItems = simpleListItems(input);
36
42
  if (listItems.length >= 2) {
37
- const markdown = generatedTodoMarkdown(listItems);
43
+ const markdown = applyGoalInstructionsToTodoMarkdown(generatedTodoMarkdown(listItems), goal);
38
44
  validateTodoMarkdown(markdown);
39
45
  return markdown;
40
46
  }
@@ -116,12 +122,134 @@ export function validateTodoMarkdown(markdown: string): void {
116
122
  });
117
123
  }
118
124
 
119
- export function buildTodoCreationPrompt(rawInput: string): string {
120
- return `Convert the following raw project request into Pi Long Task-compatible TODO markdown.\n\nRequirements:\n- Output only markdown, with no commentary and no code fence.\n- Start with exactly: # Pi Long Task TODO\n- Include a ## Progress section with one unchecked line per task: - [ ] TODO N — Title\n- Include a --- separator before task sections.\n- Create sequential sections named ## TODO N — Title.\n- Each task section must include **Goal:**, **Status:** with unchecked checkbox items, **Verify:** with concrete verification guidance, and **Done when:**.\n- Preserve any global instructions or constraints that apply to all tasks above ## Progress.\n- Keep tasks focused and independently assignable to worker sessions.\n\nRaw input:\n\n${rawInput.trim()}\n`;
125
+ export function applyGoalInstructionsToTodoMarkdown(markdown: string, goal?: string): string {
126
+ const trimmedGoal = oneLine(goal ?? "");
127
+ if (!trimmedGoal) {
128
+ return markdown;
129
+ }
130
+
131
+ let next = insertGlobalGoalInstructions(markdown, trimmedGoal);
132
+ const coverageGoal = parseCoverageGoal(trimmedGoal);
133
+ if (coverageGoal) {
134
+ next = appendCoverageVerificationToTasks(next, coverageGoalVerifyBullet(coverageGoal));
135
+ }
136
+ validateTodoMarkdown(next);
137
+ return next;
138
+ }
139
+
140
+ function insertGlobalGoalInstructions(markdown: string, goal: string): string {
141
+ const coverageGoal = parseCoverageGoal(goal);
142
+ const additions = [`- Long task goal: ${goal}`];
143
+ if (coverageGoal) {
144
+ additions.push(`- Coverage goal: ${coverageGoalAction(coverageGoal)}`);
145
+ additions.push(`- Coverage verification: ${coverageGoalVerification(coverageGoal)}`);
146
+ }
147
+
148
+ const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
149
+ const progressIndex = lines.findIndex((line) => /^##\s+Progress\s*$/i.test(line.trim()));
150
+ if (progressIndex < 0) {
151
+ return markdown;
152
+ }
153
+
154
+ const existingGlobalText = lines.slice(0, progressIndex).join("\n");
155
+ const missing = additions.filter((line) => !existingGlobalText.includes(line));
156
+ if (missing.length === 0) {
157
+ return markdown;
158
+ }
159
+
160
+ const before = trimTrailingBlankLines(lines.slice(0, progressIndex));
161
+ const after = trimLeadingBlankLines(lines.slice(progressIndex));
162
+ const hasGlobalHeading = /^Global instructions:\s*$/im.test(existingGlobalText);
163
+ const block = hasGlobalHeading ? missing : ["Global instructions:", ...missing];
164
+ return ensureTrailingNewline([...before, "", ...block, "", ...after].join("\n"));
165
+ }
166
+
167
+ function appendCoverageVerificationToTasks(markdown: string, verificationBullet: string): string {
168
+ const tasks = parseTasks(markdown);
169
+ const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
170
+ for (const task of [...tasks].reverse()) {
171
+ if (task.section.includes(verificationBullet)) {
172
+ continue;
173
+ }
174
+
175
+ const startIndex = task.startLine - 1;
176
+ const endIndex = task.endLine;
177
+ const verifyIndex = findVerifyLineIndex(lines, startIndex, endIndex);
178
+ if (verifyIndex >= 0) {
179
+ lines.splice(verifyIndex + 1, 0, verificationBullet);
180
+ continue;
181
+ }
182
+
183
+ const doneIndex = findDoneWhenLineIndex(lines, startIndex, endIndex);
184
+ const insertIndex = doneIndex >= 0 ? doneIndex : endIndex;
185
+ lines.splice(insertIndex, 0, "**Verify:**", verificationBullet, "");
186
+ }
187
+
188
+ return ensureTrailingNewline(lines.join("\n"));
189
+ }
190
+
191
+ function findVerifyLineIndex(lines: readonly string[], startIndex: number, endIndex: number): number {
192
+ for (let idx = startIndex; idx < endIndex; idx += 1) {
193
+ if (/^\*\*Verify:\*\*/i.test(lines[idx].trim())) {
194
+ return idx;
195
+ }
196
+ }
197
+ return -1;
198
+ }
199
+
200
+ function findDoneWhenLineIndex(lines: readonly string[], startIndex: number, endIndex: number): number {
201
+ for (let idx = startIndex; idx < endIndex; idx += 1) {
202
+ if (/^\*\*Done when:\*\*/i.test(lines[idx].trim())) {
203
+ return idx;
204
+ }
205
+ }
206
+ return -1;
207
+ }
208
+
209
+ function trimTrailingBlankLines(lines: string[]): string[] {
210
+ while (lines.at(-1)?.trim() === "") {
211
+ lines.pop();
212
+ }
213
+ return lines;
121
214
  }
122
215
 
123
- export function buildTodoRepairPrompt(rawInput: string, invalidOutput: string, validationError: string): string {
124
- return `Your previous response was not valid Pi Long Task TODO markdown. Correct it now.\n\nValidation/extraction error:\n${validationError.trim() || "Unknown validation error."}\n\nRequirements:\n- Output only corrected markdown, with no commentary and no code fence.\n- Start with exactly: # Pi Long Task TODO\n- Include a ## Progress section with one unchecked line per task: - [ ] TODO N — Title\n- Include a --- separator before task sections.\n- Create sequential sections named ## TODO N — Title.\n- Each task section must include **Goal:**, **Status:** with unchecked checkbox items, **Verify:** with concrete verification guidance, and **Done when:**.\n- Preserve any global instructions or constraints that apply to all tasks above ## Progress.\n- Keep tasks focused and independently assignable to worker sessions.\n\nOriginal raw input:\n\n${rawInput.trim()}\n\nPrevious invalid output:\n\n${invalidOutput.trim()}\n`;
216
+ function trimLeadingBlankLines(lines: string[]): string[] {
217
+ while (lines[0]?.trim() === "") {
218
+ lines.shift();
219
+ }
220
+ return lines;
221
+ }
222
+
223
+ function oneLine(value: string): string {
224
+ return value.replace(/\s+/g, " ").trim();
225
+ }
226
+
227
+ export function buildTodoCreationPrompt(rawInput: string, goal?: string): string {
228
+ const goalBlock = todoGoalPromptBlock(goal);
229
+ return `Convert the following raw project request into Pi Long Task-compatible TODO markdown.\n\nRequirements:\n- Output only markdown, with no commentary and no code fence.\n- Start with exactly: # Pi Long Task TODO\n- Include a ## Progress section with one unchecked line per task: - [ ] TODO N — Title\n- Include a --- separator before task sections.\n- Create sequential sections named ## TODO N — Title.\n- Each task section must include **Goal:**, **Status:** with unchecked checkbox items, **Verify:** with concrete verification guidance, and **Done when:**.\n- Preserve any global instructions or constraints that apply to all tasks above ## Progress.\n- Keep tasks focused and independently assignable to worker sessions.\n${goalBlock}\nRaw input:\n\n${rawInput.trim()}\n`;
230
+ }
231
+
232
+ export function buildTodoRepairPrompt(
233
+ rawInput: string,
234
+ invalidOutput: string,
235
+ validationError: string,
236
+ goal?: string,
237
+ ): string {
238
+ const goalBlock = todoGoalPromptBlock(goal);
239
+ return `Your previous response was not valid Pi Long Task TODO markdown. Correct it now.\n\nValidation/extraction error:\n${validationError.trim() || "Unknown validation error."}\n\nRequirements:\n- Output only corrected markdown, with no commentary and no code fence.\n- Start with exactly: # Pi Long Task TODO\n- Include a ## Progress section with one unchecked line per task: - [ ] TODO N — Title\n- Include a --- separator before task sections.\n- Create sequential sections named ## TODO N — Title.\n- Each task section must include **Goal:**, **Status:** with unchecked checkbox items, **Verify:** with concrete verification guidance, and **Done when:**.\n- Preserve any global instructions or constraints that apply to all tasks above ## Progress.\n- Keep tasks focused and independently assignable to worker sessions.\n${goalBlock}\nOriginal raw input:\n\n${rawInput.trim()}\n\nPrevious invalid output:\n\n${invalidOutput.trim()}\n`;
240
+ }
241
+
242
+ function todoGoalPromptBlock(goal: string | undefined): string {
243
+ const trimmed = goal?.trim();
244
+ if (!trimmed) {
245
+ return "";
246
+ }
247
+
248
+ const coverageGoal = parseCoverageGoal(trimmed);
249
+ const coverageBlock = coverageGoal
250
+ ? `\nCoverage goal requirements:\n- Include global instructions that tell workers to ${coverageGoalAction(coverageGoal)}\n- Include verification guidance that tells workers to ${coverageGoalVerification(coverageGoal)}\n- Do not hardcode a fixed coverage threshold; use the requested threshold (${coverageGoal.thresholdText}%).\n`
251
+ : "";
252
+ return `\nOverall goal:\n\n${trimmed}\n${coverageBlock}`;
125
253
  }
126
254
 
127
255
  export function extractAndValidateTodoMarkdown(assistantText: string): string {