pi-long-task 0.3.10 → 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 +7 -5
- package/package.json +1 -1
- package/src/goal_discovery.ts +20 -18
- package/src/goal_loop.ts +21 -2
- package/src/goal_orchestrator.ts +3 -0
- package/src/goal_review.ts +41 -0
- package/src/goal_state.ts +1 -0
- package/src/goal_todo_generation.ts +4 -1
- package/src/input_router.ts +35 -6
- package/src/render.ts +16 -1
- package/src/types.ts +9 -1
package/README.md
CHANGED
|
@@ -280,7 +280,7 @@ Use `pi_goal_task` when you have a high-level outcome instead of a ready TODO pl
|
|
|
280
280
|
Use pi_goal_task with goal "modernize the settings page, add tests, and update docs" and commit true.
|
|
281
281
|
```
|
|
282
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.
|
|
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
284
|
|
|
285
285
|
Examples:
|
|
286
286
|
|
|
@@ -289,14 +289,14 @@ Run a goal task with commits for goal: build a full Slack alternative chat app f
|
|
|
289
289
|
```
|
|
290
290
|
|
|
291
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.
|
|
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
293
|
```
|
|
294
294
|
|
|
295
295
|
```text
|
|
296
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
297
|
```
|
|
298
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.
|
|
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
300
|
|
|
301
301
|
### Discovery for vague software goals
|
|
302
302
|
|
|
@@ -335,7 +335,8 @@ Goal-loop artifacts are stored under `tmp/pi-goal-task/<goal-run-id>/`, includin
|
|
|
335
335
|
|
|
336
336
|
Safety controls:
|
|
337
337
|
|
|
338
|
-
- `
|
|
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.
|
|
339
340
|
- `timeoutMs` caps the overall goal loop; default is `172800000` ms (48 hours).
|
|
340
341
|
- `iterationTimeoutMs` caps each generated TODO worker iteration; default is `10800000` ms (3 hours).
|
|
341
342
|
- `reviewerTimeoutMs` caps each reviewer session; default is `1800000` ms (30 minutes).
|
|
@@ -365,6 +366,7 @@ Safety controls:
|
|
|
365
366
|
{
|
|
366
367
|
goal: string;
|
|
367
368
|
commit?: boolean;
|
|
369
|
+
minIterations?: number;
|
|
368
370
|
maxIterations?: number;
|
|
369
371
|
timeoutMs?: number;
|
|
370
372
|
iterationTimeoutMs?: number;
|
|
@@ -383,7 +385,7 @@ Use pi_goal_task with goal "build a full Slack alternative chat app focused on s
|
|
|
383
385
|
```
|
|
384
386
|
|
|
385
387
|
```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
|
|
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.
|
|
387
389
|
```
|
|
388
390
|
|
|
389
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.
|
package/package.json
CHANGED
package/src/goal_discovery.ts
CHANGED
|
@@ -167,7 +167,7 @@ function buildScopedRequirements(goal: string): GoalScopedRequirements {
|
|
|
167
167
|
{
|
|
168
168
|
id: "REQ-1",
|
|
169
169
|
title: "Primary product outcome",
|
|
170
|
-
description: `Define and deliver the
|
|
170
|
+
description: `Define and deliver the complete scoped software/product outcome described by the user goal: ${goal}`,
|
|
171
171
|
priority: "must",
|
|
172
172
|
acceptanceCriterionIds: ["AC-1", "AC-2"],
|
|
173
173
|
milestoneIds: ["MS-1", "MS-2"],
|
|
@@ -252,10 +252,10 @@ function buildScopedRequirements(goal: string): GoalScopedRequirements {
|
|
|
252
252
|
"Implementation workers should use this persisted specification as the definition-of-done instead of relying only on the original vague goal.",
|
|
253
253
|
],
|
|
254
254
|
openQuestions: [
|
|
255
|
-
"Who are the primary and secondary users, and what job-to-be-done should the
|
|
255
|
+
"Who are the primary and secondary users, and what job-to-be-done should the complete scoped product satisfy?",
|
|
256
256
|
"Which repository areas, platforms, integrations, or external services constrain the implementation?",
|
|
257
257
|
"What measurable product or operational signal proves the delivered slice is successful?",
|
|
258
|
-
"Which non-goals or edge cases must remain out of scope for
|
|
258
|
+
"Which non-goals or edge cases must remain out of scope for this goal-oriented delivery?",
|
|
259
259
|
],
|
|
260
260
|
};
|
|
261
261
|
}
|
|
@@ -338,7 +338,7 @@ function buildAcceptanceCriteria(goal: string): GoalAcceptanceCriterion[] {
|
|
|
338
338
|
{
|
|
339
339
|
id: "AC-6",
|
|
340
340
|
description:
|
|
341
|
-
"Relevant launch, positioning, target segment, and growth metric context is available or explicitly deemed unnecessary for the
|
|
341
|
+
"Relevant launch, positioning, target segment, and growth metric context is available or explicitly deemed unnecessary for the scoped goal.",
|
|
342
342
|
requirementIds: ["REQ-6"],
|
|
343
343
|
verificationGateIds: ["VG-5"],
|
|
344
344
|
},
|
|
@@ -409,7 +409,7 @@ function buildDesignConstraints(goal: string): GoalDesignConstraints {
|
|
|
409
409
|
uxPrinciples: [
|
|
410
410
|
"Optimize for the primary user workflow before secondary or administrative flows.",
|
|
411
411
|
"Prefer clear, reversible, and observable interactions over hidden automation.",
|
|
412
|
-
"Keep
|
|
412
|
+
"Keep UX states explicit enough for implementation workers to verify without stakeholder interviews.",
|
|
413
413
|
],
|
|
414
414
|
uiRequirements: [
|
|
415
415
|
`Any user-facing UI for '${goal}' should use existing project components, layout conventions, and content tone where available.`,
|
|
@@ -433,7 +433,7 @@ function buildDesignConstraints(goal: string): GoalDesignConstraints {
|
|
|
433
433
|
},
|
|
434
434
|
{
|
|
435
435
|
id: "DC-2",
|
|
436
|
-
title: "Accessible
|
|
436
|
+
title: "Accessible scoped product",
|
|
437
437
|
description:
|
|
438
438
|
"Do not mark user-facing work complete without considering keyboard, screen-reader, and contrast needs.",
|
|
439
439
|
},
|
|
@@ -457,8 +457,8 @@ function buildProductConstraints(goal: string): GoalProductConstraints {
|
|
|
457
457
|
"Pi goal-task orchestration artifacts under the run directory.",
|
|
458
458
|
],
|
|
459
459
|
businessRules: [
|
|
460
|
-
"
|
|
461
|
-
"Out-of-scope enhancements require a later goal or explicit stakeholder decision
|
|
460
|
+
"All behavior required to satisfy the scoped goal should be implemented, while unrelated expansion stays out of scope.",
|
|
461
|
+
"Out-of-scope enhancements require a later goal or explicit stakeholder decision",
|
|
462
462
|
],
|
|
463
463
|
compliance: [
|
|
464
464
|
"Respect existing authorization, privacy, data-retention, and safety constraints present in the repository.",
|
|
@@ -476,8 +476,9 @@ function buildProductConstraints(goal: string): GoalProductConstraints {
|
|
|
476
476
|
constraints: [
|
|
477
477
|
{
|
|
478
478
|
id: "PC-1",
|
|
479
|
-
title: "Scoped
|
|
480
|
-
description:
|
|
479
|
+
title: "Scoped full-goal delivery",
|
|
480
|
+
description:
|
|
481
|
+
"Deliver the complete scoped goal rather than downgrading broad user intent to an MVP or speculative roadmap.",
|
|
481
482
|
},
|
|
482
483
|
{
|
|
483
484
|
id: "PC-2",
|
|
@@ -497,11 +498,11 @@ function buildMarketingGrowthContext(goal: string): GoalMarketingGrowthContext {
|
|
|
497
498
|
return {
|
|
498
499
|
targetSegments: [
|
|
499
500
|
`Users, teams, or operators whose workflow improves when '${goal}' is delivered.`,
|
|
500
|
-
"Early internal adopters or reviewers who can validate the
|
|
501
|
+
"Early internal adopters or reviewers who can validate the delivered scoped product.",
|
|
501
502
|
],
|
|
502
503
|
positioning: [
|
|
503
|
-
"A scoped software
|
|
504
|
-
"Avoid
|
|
504
|
+
"A scoped software product outcome with clear user value and reviewable definition-of-done.",
|
|
505
|
+
"Avoid downgrading explicit user intent to a partial MVP unless the user asked for an MVP.",
|
|
505
506
|
],
|
|
506
507
|
acquisitionChannels: [
|
|
507
508
|
"Release notes, README/update documentation, in-app messaging, or internal handoff notes as appropriate.",
|
|
@@ -525,10 +526,11 @@ function buildDiscoveryConsolidation(goal: string): GoalDiscoveryConsolidation {
|
|
|
525
526
|
{
|
|
526
527
|
role: "product_owner",
|
|
527
528
|
title: "Product Owner",
|
|
528
|
-
objective:
|
|
529
|
+
objective:
|
|
530
|
+
"Define user value, full scoped product requirements, non-goals, and product acceptance for the vague goal.",
|
|
529
531
|
findings: [
|
|
530
532
|
`The goal '${goal}' needs a primary user outcome before implementation begins.`,
|
|
531
|
-
"Scope should
|
|
533
|
+
"Scope should preserve explicit user requirements as mandatory and clarify any unresolved stakeholder questions.",
|
|
532
534
|
],
|
|
533
535
|
decisions: [
|
|
534
536
|
"Use REQ-1 as the core product outcome and AC-1 as the primary acceptance criterion.",
|
|
@@ -573,7 +575,7 @@ function buildDiscoveryConsolidation(goal: string): GoalDiscoveryConsolidation {
|
|
|
573
575
|
"Use REQ-3 and AC-4 to constrain technical design.",
|
|
574
576
|
"Make architecture/regression review a required gate.",
|
|
575
577
|
],
|
|
576
|
-
risks: ["New dependencies or rewrites could exceed the
|
|
578
|
+
risks: ["New dependencies or rewrites could exceed the scoped product requirements."],
|
|
577
579
|
requirementIds: ["REQ-3"],
|
|
578
580
|
milestoneIds: ["MS-2"],
|
|
579
581
|
acceptanceCriterionIds: ["AC-4", "AC-5"],
|
|
@@ -587,7 +589,7 @@ function buildDiscoveryConsolidation(goal: string): GoalDiscoveryConsolidation {
|
|
|
587
589
|
"Translate vague product intent into workflow, UI state, accessibility, and design-system constraints.",
|
|
588
590
|
findings: [
|
|
589
591
|
"User-facing work needs explicit workflow states and accessibility expectations.",
|
|
590
|
-
"Existing design conventions should guide
|
|
592
|
+
"Existing design conventions should guide product UI unless the discovered requirements call for a new visual direction.",
|
|
591
593
|
],
|
|
592
594
|
decisions: [
|
|
593
595
|
"Use REQ-4 and AC-3 to keep UX/design review in scope.",
|
|
@@ -655,7 +657,7 @@ function buildDefinitionOfDone(
|
|
|
655
657
|
): GoalDefinitionOfDone {
|
|
656
658
|
return {
|
|
657
659
|
summary:
|
|
658
|
-
"Done when all must/should scoped requirements are implemented
|
|
660
|
+
"Done when all must/should scoped requirements are implemented, acceptance criteria are satisfied, required verification gates pass or record justified blockers, and review evaluates the result against this persisted product definition. Do not treat a partial MVP as complete when the original goal requests a broader product.",
|
|
659
661
|
requirementIds,
|
|
660
662
|
acceptanceCriterionIds,
|
|
661
663
|
verificationGateIds: requiredGateIds,
|
package/src/goal_loop.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { CoordinatorStatus } from "./types.ts";
|
|
|
5
5
|
export const GOAL_LOOP_STATE_SCHEMA_VERSION = 1;
|
|
6
6
|
|
|
7
7
|
export const DEFAULT_GOAL_LOOP_LIMITS = {
|
|
8
|
+
minIterations: 1,
|
|
8
9
|
maxIterations: 50,
|
|
9
10
|
timeoutMs: 172_800_000,
|
|
10
11
|
iterationTimeoutMs: 10_800_000,
|
|
@@ -35,6 +36,7 @@ export type GoalIterationStatus =
|
|
|
35
36
|
export type GoalLoopStopKind = "cancelled" | "timeout" | "max_iterations" | "complete";
|
|
36
37
|
|
|
37
38
|
export interface GoalLoopLimits {
|
|
39
|
+
minIterations: number;
|
|
38
40
|
maxIterations: number;
|
|
39
41
|
timeoutMs: number;
|
|
40
42
|
iterationTimeoutMs: number;
|
|
@@ -42,6 +44,7 @@ export interface GoalLoopLimits {
|
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
export interface GoalLoopLimitInput {
|
|
47
|
+
minIterations?: number;
|
|
45
48
|
maxIterations?: number;
|
|
46
49
|
timeoutMs?: number;
|
|
47
50
|
iterationTimeoutMs?: number;
|
|
@@ -169,8 +172,16 @@ export class GoalLoopStateError extends Error {
|
|
|
169
172
|
}
|
|
170
173
|
|
|
171
174
|
export function normalizeGoalLoopLimits(input: GoalLoopLimitInput = {}): GoalLoopLimits {
|
|
175
|
+
const explicitMaxIterations = optionalPositiveInteger(input.maxIterations);
|
|
176
|
+
const minIterations = positiveInteger(
|
|
177
|
+
input.minIterations,
|
|
178
|
+
explicitMaxIterations ?? DEFAULT_GOAL_LOOP_LIMITS.minIterations,
|
|
179
|
+
);
|
|
180
|
+
const maxIterations = Math.max(minIterations, explicitMaxIterations ?? DEFAULT_GOAL_LOOP_LIMITS.maxIterations);
|
|
181
|
+
|
|
172
182
|
return {
|
|
173
|
-
|
|
183
|
+
minIterations,
|
|
184
|
+
maxIterations,
|
|
174
185
|
timeoutMs: positiveMilliseconds(input.timeoutMs, DEFAULT_GOAL_LOOP_LIMITS.timeoutMs),
|
|
175
186
|
iterationTimeoutMs: positiveMilliseconds(input.iterationTimeoutMs, DEFAULT_GOAL_LOOP_LIMITS.iterationTimeoutMs),
|
|
176
187
|
reviewerTimeoutMs: positiveMilliseconds(input.reviewerTimeoutMs, DEFAULT_GOAL_LOOP_LIMITS.reviewerTimeoutMs),
|
|
@@ -442,6 +453,10 @@ export function validateGoalLoopState(value: unknown): GoalLoopState {
|
|
|
442
453
|
if (!state.limits || typeof state.limits !== "object") {
|
|
443
454
|
throw new GoalLoopStateError("Goal loop state is missing limits.");
|
|
444
455
|
}
|
|
456
|
+
state.limits = normalizeGoalLoopLimits({
|
|
457
|
+
...state.limits,
|
|
458
|
+
minIterations: state.limits.minIterations ?? DEFAULT_GOAL_LOOP_LIMITS.minIterations,
|
|
459
|
+
});
|
|
445
460
|
if (!Array.isArray(state.iterations) || !Array.isArray(state.trace)) {
|
|
446
461
|
throw new GoalLoopStateError("Goal loop state is missing iterations or trace arrays.");
|
|
447
462
|
}
|
|
@@ -553,10 +568,14 @@ function withTrace(state: GoalLoopState, event: GoalLoopTraceEvent): GoalLoopSta
|
|
|
553
568
|
}
|
|
554
569
|
|
|
555
570
|
function positiveInteger(value: number | undefined, fallback: number): number {
|
|
571
|
+
return optionalPositiveInteger(value) ?? fallback;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function optionalPositiveInteger(value: number | undefined): number | undefined {
|
|
556
575
|
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
557
576
|
return Math.floor(value);
|
|
558
577
|
}
|
|
559
|
-
return
|
|
578
|
+
return undefined;
|
|
560
579
|
}
|
|
561
580
|
|
|
562
581
|
function positiveMilliseconds(value: number | undefined, fallback: number): number {
|
package/src/goal_orchestrator.ts
CHANGED
|
@@ -52,6 +52,7 @@ export interface GoalLoopProgressUpdate {
|
|
|
52
52
|
status: GoalLoopStatus;
|
|
53
53
|
currentIteration: number;
|
|
54
54
|
totalIterations: number;
|
|
55
|
+
minIterations: number;
|
|
55
56
|
maxIterations: number;
|
|
56
57
|
limits: GoalLoopLimits;
|
|
57
58
|
resultPath: string;
|
|
@@ -125,6 +126,7 @@ export async function runGoalLoop(options: RunGoalLoopOptions): Promise<GoalLoop
|
|
|
125
126
|
goalRunId:
|
|
126
127
|
options.goalRunId ?? `goal-${new Date().toISOString().replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`,
|
|
127
128
|
goalRunDir: options.goalRunDir,
|
|
129
|
+
minIterations: options.minIterations,
|
|
128
130
|
maxIterations: options.maxIterations,
|
|
129
131
|
timeoutMs: options.timeoutMs,
|
|
130
132
|
iterationTimeoutMs: options.iterationTimeoutMs,
|
|
@@ -155,6 +157,7 @@ export async function runGoalLoop(options: RunGoalLoopOptions): Promise<GoalLoop
|
|
|
155
157
|
status: state.status,
|
|
156
158
|
currentIteration: state.currentIteration,
|
|
157
159
|
totalIterations: state.iterations.length,
|
|
160
|
+
minIterations: state.limits.minIterations,
|
|
158
161
|
maxIterations: state.limits.maxIterations,
|
|
159
162
|
limits: state.limits,
|
|
160
163
|
resultPath: store.paths.resultPath,
|
package/src/goal_review.ts
CHANGED
|
@@ -180,6 +180,8 @@ export async function runGoalReviewSession(options: GoalReviewOptions): Promise<
|
|
|
180
180
|
});
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
reviewerResult = enforceMinimumIterationsBeforeCompletion(reviewerResult, state, iteration.iteration);
|
|
184
|
+
|
|
183
185
|
state = recordReviewerResult(state, iteration.iteration, reviewerResult, { now: now() });
|
|
184
186
|
await persistStateChange(store, previousTraceLength, state);
|
|
185
187
|
const updatedIteration = currentIteration(state, iteration.iteration);
|
|
@@ -220,6 +222,11 @@ export function buildGoalReviewTaskPayload(options: {
|
|
|
220
222
|
const reviewTargetInstruction = options.goalSpecification
|
|
221
223
|
? "Review whether the latest worker run satisfies the persisted goal specification and definition-of-done. Treat the persisted specification as the primary review target; keep the original high-level goal available only as traceability/context."
|
|
222
224
|
: "Review whether the original high-level goal is complete after the latest worker run.";
|
|
225
|
+
const iterationPolicy = `Goal loop iteration policy:
|
|
226
|
+
- Current iteration: ${iteration.iteration}
|
|
227
|
+
- Minimum iterations before completion may stop the loop: ${state.limits.minIterations}
|
|
228
|
+
- Maximum iterations: ${state.limits.maxIterations}
|
|
229
|
+
- If current iteration is below the minimum, do not return "complete" even if the current implementation looks good. Return "incomplete" with concrete remainingWork for the next improvement pass, such as missing verification, hardening, UX polish, security, performance, docs, edge cases, or maintainability follow-up.`;
|
|
223
230
|
const decisionRules = options.goalSpecification
|
|
224
231
|
? `- Use "complete" only when the persisted definition-of-done is satisfied, including in-scope requirements, milestones, acceptance criteria, required verification gates, and applicable design/product constraints.
|
|
225
232
|
- Use "incomplete" when any required spec requirement, milestone, acceptance criterion, verification gate, artifact, or constraint still needs work and another TODO-generation iteration should be started.
|
|
@@ -242,6 +249,8 @@ ${specificationBlock}
|
|
|
242
249
|
Goal run: ${state.goalRunId}
|
|
243
250
|
Iteration: ${iteration.iteration}
|
|
244
251
|
Generated TODO path: ${generatedTodo?.todoPath ?? "unknown"}
|
|
252
|
+
|
|
253
|
+
${iterationPolicy}
|
|
245
254
|
Worker result:
|
|
246
255
|
|
|
247
256
|
${markdownFence(JSON.stringify(workerResult ?? null, null, 2), "json")}
|
|
@@ -279,6 +288,38 @@ export function parseGoalReviewerOutput(
|
|
|
279
288
|
};
|
|
280
289
|
}
|
|
281
290
|
|
|
291
|
+
function enforceMinimumIterationsBeforeCompletion(
|
|
292
|
+
result: GoalReviewerResultState,
|
|
293
|
+
state: GoalLoopState,
|
|
294
|
+
iteration: number,
|
|
295
|
+
): GoalReviewerResultState {
|
|
296
|
+
if (result.decision !== "complete" && !result.complete) {
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
if (iteration >= state.limits.minIterations) {
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const remainingIterations = Math.max(1, state.limits.minIterations - iteration);
|
|
304
|
+
const remainingWork = result.remainingWork.length > 0 ? result.remainingWork : minimumIterationRemainingWork(state);
|
|
305
|
+
return {
|
|
306
|
+
...result,
|
|
307
|
+
decision: "incomplete",
|
|
308
|
+
complete: false,
|
|
309
|
+
summary: `Minimum iteration target not reached (${iteration}/${state.limits.minIterations}); continuing goal loop.`,
|
|
310
|
+
rationale: `${result.rationale}\n\nReviewer completion was deferred because this goal loop requires at least ${state.limits.minIterations} iteration(s) before it may stop. ${remainingIterations} more iteration(s) are required, so the next pass should look for concrete improvements, hardening, verification, and polish rather than stopping early.`,
|
|
311
|
+
remainingWork,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function minimumIterationRemainingWork(state: GoalLoopState): string[] {
|
|
316
|
+
return [
|
|
317
|
+
`Continue toward the required minimum of ${state.limits.minIterations} goal-loop iterations before accepting completion.`,
|
|
318
|
+
"Run another pass focused on gaps the previous TODO did not cover: verification depth, edge cases, security, performance, UX polish, documentation, maintainability, and product completeness.",
|
|
319
|
+
"Generate concrete implementation or review tasks from the persisted goal specification and latest evidence instead of stopping after the first apparently complete pass.",
|
|
320
|
+
];
|
|
321
|
+
}
|
|
322
|
+
|
|
282
323
|
export async function runGoalReviewerSession(options: GoalReviewerRunnerOptions): Promise<GoalReviewerSessionResult> {
|
|
283
324
|
const sessionFactory = options.sessionFactory ?? createIsolatedWorkerSession;
|
|
284
325
|
const events: unknown[] = [];
|
package/src/goal_state.ts
CHANGED
|
@@ -112,6 +112,7 @@ export class GoalStateStore {
|
|
|
112
112
|
"",
|
|
113
113
|
"## Safety limits",
|
|
114
114
|
"",
|
|
115
|
+
`- Minimum iterations before completion: ${state.limits.minIterations}`,
|
|
115
116
|
`- Max iterations: ${state.limits.maxIterations}`,
|
|
116
117
|
`- Run timeout: ${state.limits.timeoutMs}ms`,
|
|
117
118
|
`- Iteration timeout: ${state.limits.iterationTimeoutMs}ms`,
|
|
@@ -147,7 +147,7 @@ export async function runGoalTodoGenerationLongTask(
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
export function buildGoalTodoGenerationTaskPayload(options: {
|
|
150
|
-
state: Pick<GoalLoopState, "goal" | "goalRunId">;
|
|
150
|
+
state: Pick<GoalLoopState, "goal" | "goalRunId" | "limits">;
|
|
151
151
|
iteration: number;
|
|
152
152
|
outputPath: string;
|
|
153
153
|
additionalContext?: string;
|
|
@@ -162,6 +162,8 @@ export function buildGoalTodoGenerationTaskPayload(options: {
|
|
|
162
162
|
"markdown",
|
|
163
163
|
)}\n`
|
|
164
164
|
: "";
|
|
165
|
+
const iterationPolicy = `- Goal-loop iteration target: generate implementation work for iteration ${options.iteration} of at least ${options.state.limits.minIterations} required iteration(s), with a hard maximum of ${options.state.limits.maxIterations}.
|
|
166
|
+
- Do not generate a no-op TODO merely because a previous reviewer believed the goal was complete before the minimum iteration target. Always look for the next concrete improvement, hardening, verification, UX, security, performance, docs, maintainability, or product-completeness pass until the minimum is reached.`;
|
|
165
167
|
|
|
166
168
|
return `# Pi Long Task TODO
|
|
167
169
|
|
|
@@ -173,6 +175,7 @@ export function buildGoalTodoGenerationTaskPayload(options: {
|
|
|
173
175
|
- Write the generated Pi Long Task-compatible TODO markdown to \`${options.outputPath}\`.
|
|
174
176
|
- Do not wrap the generated file in a code fence and do not include commentary outside the TODO markdown in that file.
|
|
175
177
|
- Keep generated tasks focused, independently assignable, and safe for separate worker sessions.
|
|
178
|
+
${iterationPolicy}
|
|
176
179
|
${
|
|
177
180
|
options.goalSpecification
|
|
178
181
|
? "- A persisted goal specification is available; derive implementation TODOs from that specification rather than only the original vague goal.\n- Ensure generated tasks explicitly cover relevant requirement IDs, milestones, acceptance criteria, verification gates, constraints, and definition-of-done items from the specification.\n- Include spec IDs (for example REQ-*, MS-*, AC-*, VG-*) in generated task goals/status/verification/done-when guidance wherever applicable.\n"
|
package/src/input_router.ts
CHANGED
|
@@ -21,6 +21,8 @@ const COMMIT_TRUE_RE =
|
|
|
21
21
|
const GOAL_RE = /\b(?:with\s+(?:the\s+)?goal|goal)\s*(?::|=|\b(?:to|of|for|that)\b)\s*([\s\S]+)$/i;
|
|
22
22
|
const TRAILING_COMMIT_MODIFIER_RE =
|
|
23
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;
|
|
24
26
|
|
|
25
27
|
export interface ParsedLongTaskRequestOptions {
|
|
26
28
|
commit: boolean;
|
|
@@ -28,6 +30,7 @@ export interface ParsedLongTaskRequestOptions {
|
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export interface ParsedGoalTaskRequestOptions extends ParsedLongTaskRequestOptions {
|
|
33
|
+
minIterations?: number;
|
|
31
34
|
maxIterations?: number;
|
|
32
35
|
}
|
|
33
36
|
|
|
@@ -60,10 +63,13 @@ export function parseLongTaskRequestOptions(text: string): ParsedLongTaskRequest
|
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
export function parseGoalTaskRequestOptions(text: string): ParsedGoalTaskRequestOptions {
|
|
66
|
+
const maxIterations = inferMaxIterations(text);
|
|
67
|
+
const minIterations = inferMinIterations(text) ?? inferExactIterationCount(text) ?? maxIterations;
|
|
63
68
|
return {
|
|
64
69
|
commit: inferCommitSetting(text) ?? true,
|
|
65
70
|
goal: inferGoalSetting(text) ?? inferGoalLoopText(text),
|
|
66
|
-
|
|
71
|
+
minIterations,
|
|
72
|
+
maxIterations,
|
|
67
73
|
};
|
|
68
74
|
}
|
|
69
75
|
|
|
@@ -114,17 +120,38 @@ export function inferGoalSetting(text: string): string | undefined {
|
|
|
114
120
|
|
|
115
121
|
function normalizeGoalText(text: string): string | undefined {
|
|
116
122
|
let goal = text.trim();
|
|
117
|
-
while (TRAILING_COMMIT_MODIFIER_RE.test(goal)) {
|
|
118
|
-
goal = goal.replace(TRAILING_COMMIT_MODIFIER_RE, "").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();
|
|
119
125
|
}
|
|
120
126
|
goal = goal.replace(/^["'“”‘’]+|["'“”‘’.,;:!?]+$/g, "").trim();
|
|
121
127
|
return goal || undefined;
|
|
122
128
|
}
|
|
123
129
|
|
|
124
130
|
function inferMaxIterations(text: string): number | undefined {
|
|
125
|
-
const match = /\bmax(?:imum)?\s+(?:iterations?|loops?|passes?)\s*(?::|=|of|to)?\s*(\d+)\b/i.exec(text);
|
|
126
|
-
|
|
127
|
-
|
|
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;
|
|
128
155
|
}
|
|
129
156
|
|
|
130
157
|
function inferGoalLoopText(text: string): string | undefined {
|
|
@@ -158,11 +185,13 @@ function buildGoalTaskToolPrompt(originalText: string, options: ParsedGoalTaskRe
|
|
|
158
185
|
const goalLine = options.goal
|
|
159
186
|
? `Set goal to ${JSON.stringify(options.goal)}.`
|
|
160
187
|
: "Set goal from the original request.";
|
|
188
|
+
const minIterationsLine = options.minIterations ? [`Set minIterations to ${options.minIterations}.`] : [];
|
|
161
189
|
const maxIterationsLine = options.maxIterations ? [`Set maxIterations to ${options.maxIterations}.`] : [];
|
|
162
190
|
return [
|
|
163
191
|
"Use the pi_goal_task tool for this request.",
|
|
164
192
|
goalLine,
|
|
165
193
|
`Set commit to ${options.commit ? "true" : "false"}.`,
|
|
194
|
+
...minIterationsLine,
|
|
166
195
|
...maxIterationsLine,
|
|
167
196
|
"The goal loop will generate TODO markdown, execute it as a long task, review completion, and repeat until complete or stopped by limits.",
|
|
168
197
|
"Do not perform the work directly outside pi_goal_task.",
|
package/src/render.ts
CHANGED
|
@@ -126,6 +126,7 @@ export interface GoalTaskToolRenderDetails {
|
|
|
126
126
|
status: GoalLoopStatus;
|
|
127
127
|
currentIteration: number;
|
|
128
128
|
totalIterations: number;
|
|
129
|
+
minIterations: number;
|
|
129
130
|
maxIterations: number;
|
|
130
131
|
resultPath: string;
|
|
131
132
|
statePath?: string;
|
|
@@ -145,6 +146,7 @@ export function formatGoalLoopResultMessage(result: GoalLoopResultForRendering):
|
|
|
145
146
|
`Pi Goal Task: ${state.status}`,
|
|
146
147
|
`Goal: ${state.goal}`,
|
|
147
148
|
`Iterations: ${state.iterations.length}/${state.limits.maxIterations}`,
|
|
149
|
+
`Minimum iterations: ${state.limits.minIterations}`,
|
|
148
150
|
`Result file: ${result.resultPath}`,
|
|
149
151
|
`State file: ${state.goalRunDir}/GOAL_STATE.json`,
|
|
150
152
|
];
|
|
@@ -174,6 +176,7 @@ export function goalTaskDetailsFromResult(result: GoalLoopResultForRendering): G
|
|
|
174
176
|
status: result.state.status,
|
|
175
177
|
currentIteration: result.state.currentIteration,
|
|
176
178
|
totalIterations: result.state.iterations.length,
|
|
179
|
+
minIterations: result.state.limits.minIterations,
|
|
177
180
|
maxIterations: result.state.limits.maxIterations,
|
|
178
181
|
resultPath: result.resultPath,
|
|
179
182
|
statePath: `${result.state.goalRunDir}/GOAL_STATE.json`,
|
|
@@ -188,12 +191,20 @@ export function goalTaskDetailsFromResult(result: GoalLoopResultForRendering): G
|
|
|
188
191
|
}
|
|
189
192
|
|
|
190
193
|
export function renderGoalTaskToolCall(
|
|
191
|
-
args: {
|
|
194
|
+
args: {
|
|
195
|
+
goal?: string;
|
|
196
|
+
commit?: boolean;
|
|
197
|
+
minIterations?: number;
|
|
198
|
+
maxIterations?: number;
|
|
199
|
+
timeoutMs?: number;
|
|
200
|
+
reviewerTimeoutMs?: number;
|
|
201
|
+
},
|
|
192
202
|
theme: Theme,
|
|
193
203
|
): Text {
|
|
194
204
|
const commit = (args.commit ?? true) ? theme.fg("warning", "commit:on") : theme.fg("dim", "commit:off");
|
|
195
205
|
const goal = oneLine(args.goal ?? "");
|
|
196
206
|
const limits = [
|
|
207
|
+
args.minIterations ? `min:${args.minIterations}` : undefined,
|
|
197
208
|
args.maxIterations ? `max:${args.maxIterations}` : undefined,
|
|
198
209
|
args.timeoutMs ? `timeout:${args.timeoutMs}ms` : undefined,
|
|
199
210
|
args.reviewerTimeoutMs ? `review:${args.reviewerTimeoutMs}ms` : undefined,
|
|
@@ -227,11 +238,13 @@ function renderGoalTaskProgress(details: Record<string, unknown> | undefined, fa
|
|
|
227
238
|
const message = stringValue(details?.message) || firstLine(fallback) || "Pi Goal Task is running...";
|
|
228
239
|
const phase = stringValue(details?.phase);
|
|
229
240
|
const iteration = numberValue(details?.iteration) ?? numberValue(details?.currentIteration);
|
|
241
|
+
const minIterations = numberValue(details?.minIterations);
|
|
230
242
|
const maxIterations = numberValue(details?.maxIterations);
|
|
231
243
|
const status = stringValue(details?.status) || "running";
|
|
232
244
|
const cost = numberValue(details?.totalCost);
|
|
233
245
|
const meta = [
|
|
234
246
|
iteration ? `iteration ${iteration}${maxIterations ? `/${maxIterations}` : ""}` : undefined,
|
|
247
|
+
minIterations && iteration && iteration < minIterations ? `min:${minIterations}` : undefined,
|
|
235
248
|
status,
|
|
236
249
|
cost && cost > 0 ? formatCost(cost) : undefined,
|
|
237
250
|
]
|
|
@@ -249,6 +262,7 @@ function renderGoalTaskSummary(details: GoalTaskToolRenderDetails, expanded: boo
|
|
|
249
262
|
const summary = [
|
|
250
263
|
`${theme.fg(statusStyle, icon)} ${theme.fg("toolTitle", theme.bold("Pi Goal Task"))} ${theme.fg(statusStyle, details.status)}`,
|
|
251
264
|
theme.fg("muted", `${details.totalIterations}/${details.maxIterations} iterations`),
|
|
265
|
+
details.totalIterations < details.minIterations ? theme.fg("muted", `min:${details.minIterations}`) : undefined,
|
|
252
266
|
details.latestReviewerDecision ? theme.fg("muted", `review:${details.latestReviewerDecision}`) : undefined,
|
|
253
267
|
details.totalCost ? theme.fg("muted", `spend ${formatCost(details.totalCost)}`) : undefined,
|
|
254
268
|
].filter(Boolean);
|
|
@@ -295,6 +309,7 @@ function goalTaskDetails(details: Record<string, unknown> | undefined): GoalTask
|
|
|
295
309
|
status,
|
|
296
310
|
currentIteration: numberValue(details.currentIteration) ?? 0,
|
|
297
311
|
totalIterations: numberValue(details.totalIterations) ?? 0,
|
|
312
|
+
minIterations: numberValue(details.minIterations) ?? 0,
|
|
298
313
|
maxIterations: numberValue(details.maxIterations) ?? 0,
|
|
299
314
|
resultPath,
|
|
300
315
|
statePath: stringValue(details.statePath),
|
package/src/types.ts
CHANGED
|
@@ -37,10 +37,18 @@ export const PiGoalTaskParams = Type.Object(
|
|
|
37
37
|
"Whether worker long tasks may commit completed TODO work during the goal loop. Defaults to true for goal loops.",
|
|
38
38
|
}),
|
|
39
39
|
),
|
|
40
|
+
minIterations: Type.Optional(
|
|
41
|
+
Type.Integer({
|
|
42
|
+
minimum: 1,
|
|
43
|
+
description:
|
|
44
|
+
"Minimum number of generate → execute → review iterations before a complete review may stop the loop. If omitted while maxIterations is provided, maxIterations is also used as the minimum target.",
|
|
45
|
+
}),
|
|
46
|
+
),
|
|
40
47
|
maxIterations: Type.Optional(
|
|
41
48
|
Type.Integer({
|
|
42
49
|
minimum: 1,
|
|
43
|
-
description:
|
|
50
|
+
description:
|
|
51
|
+
"Maximum number of generate → execute → review iterations before stopping. Defaults to 50; when explicitly provided without minIterations, it also acts as the minimum target.",
|
|
44
52
|
}),
|
|
45
53
|
),
|
|
46
54
|
timeoutMs: Type.Optional(
|