pi-plan-task 1.0.2 → 1.1.0
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 +26 -10
- package/extensions/build-session.test.ts +25 -8
- package/extensions/build-session.ts +10 -3
- package/extensions/index.ts +52 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,15 +29,15 @@ Or add the source to `packages` in `~/.pi/agent/settings.json`.
|
|
|
29
29
|
| Command | What it does |
|
|
30
30
|
| --- | --- |
|
|
31
31
|
| `/plan [file or goal]` | Read-only planning. Writes `.plan_task/plan.md` and `.plan_task/task.md`. |
|
|
32
|
-
| `/build` |
|
|
33
|
-
| `/build
|
|
34
|
-
| `/
|
|
35
|
-
| `/goal` |
|
|
32
|
+
| `/build` | Execute the next unfinished task in this session, then ask whether to continue here or in a new session. |
|
|
33
|
+
| `/build new` | Same as `/build`, but start that task in a new session. |
|
|
34
|
+
| `/goal` | Execute remaining tasks until `task.md` is complete. No session prompts. All in this session. |
|
|
35
|
+
| `/goal new` | Same as `/goal`, but each remaining task starts in a new session. |
|
|
36
36
|
| `/tasks` | Show the current task list and progress. |
|
|
37
37
|
|
|
38
|
-
Restarting Pi does not auto-start work. Run `/build` again.
|
|
38
|
+
Restarting Pi does not auto-start work. Run `/build` or `/goal` again.
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
Session prompts only happen after a `/build` task finishes. `/goal` and `/goal new` never ask. Those prompts use this package's `select` dialog, not `ask_user_question`.
|
|
41
41
|
|
|
42
42
|
### `/plan` arguments
|
|
43
43
|
|
|
@@ -51,11 +51,27 @@ Restarting Pi does not auto-start work. Run `/build` again.
|
|
|
51
51
|
|
|
52
52
|
If the first argument is an existing file — or looks like a path such as `./spec.md`, `notes.txt`, or `@spec.md` — that file is the spec. Anything after it is extra planning guidance. Missing explicit paths are rejected instead of being treated as a prompt.
|
|
53
53
|
|
|
54
|
+
### `/build` and `/goal`
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
/build
|
|
58
|
+
/build new
|
|
59
|
+
/goal
|
|
60
|
+
/goal new
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`new` and `--new` are equivalent.
|
|
64
|
+
|
|
65
|
+
- `/build` runs one task here. After it is marked complete, Pi asks whether to continue in this session or a new session.
|
|
66
|
+
- `/build new` opens a new session for that one task, then asks the same question when it finishes.
|
|
67
|
+
- `/goal` runs every remaining task in this session.
|
|
68
|
+
- `/goal new` opens a new session for the next unfinished task, then another new session after each completed task, until the list is done.
|
|
69
|
+
|
|
54
70
|
## Planning method
|
|
55
71
|
|
|
56
72
|
`/plan` uses `extensions/planning-and-task-breakdown.md` as the planning prompt.
|
|
57
73
|
|
|
58
|
-
That file keeps the same section headings as `planning-and-task-breakdown.md`, so later methodology edits can be copied section-by-section from that source. Plan-task only changes output paths and the checklist form `/build`
|
|
74
|
+
That file keeps the same section headings as `planning-and-task-breakdown.md`, so later methodology edits can be copied section-by-section from that source. Plan-task only changes output paths and the checklist form `/build` and `/goal` need:
|
|
59
75
|
|
|
60
76
|
- `.plan_task/plan.md` and `.plan_task/task.md`
|
|
61
77
|
- checklist lines in the form `- [ ] N. Title`
|
|
@@ -66,7 +82,7 @@ This package does not install or load a skill. It also does not modify Pi's syst
|
|
|
66
82
|
|
|
67
83
|
Phase instructions are conversation messages, not system-prompt patches:
|
|
68
84
|
|
|
69
|
-
- `/plan
|
|
85
|
+
- `/plan`, `/build`, and `/goal` send a short visible user message to start the turn.
|
|
70
86
|
- The planning method or current-task instructions are injected once, as a hidden message, when that phase or task starts.
|
|
71
87
|
- Later turns in the same phase inject nothing unless the checklist actually changed; then a small build-status message is appended.
|
|
72
88
|
- `context` keeps only the newest framing for the current phase. Stale plan/build messages are dropped from the model context. While idle, every injected message is filtered out.
|
|
@@ -75,7 +91,7 @@ Session history still stores the injected messages. Filtering is non-destructive
|
|
|
75
91
|
|
|
76
92
|
## Ask user
|
|
77
93
|
|
|
78
|
-
`/plan
|
|
94
|
+
`/plan`, `/build`, and `/goal` keep juicesharp's `ask_user_question` available and tell the model to call it for consequential choices the repo cannot answer:
|
|
79
95
|
|
|
80
96
|
- 1-4 questions per call, each with a short `header` and 2-4 described options
|
|
81
97
|
- recommended option first, with `(Recommended)` on the label
|
|
@@ -99,7 +115,7 @@ Created in the current project:
|
|
|
99
115
|
- [ ] 2. Add login UI
|
|
100
116
|
```
|
|
101
117
|
|
|
102
|
-
`/build`
|
|
118
|
+
`/build` and `/goal` resume from the first unchecked item.
|
|
103
119
|
|
|
104
120
|
## Config
|
|
105
121
|
|
|
@@ -1,18 +1,35 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
|
-
import { parseBuildPlacement } from "./build-session.ts";
|
|
3
|
+
import { parseBuildPlacement, parseGoalMode } from "./build-session.ts";
|
|
4
4
|
|
|
5
5
|
describe("parseBuildPlacement", () => {
|
|
6
|
-
it("
|
|
7
|
-
assert.equal(parseBuildPlacement(""), "
|
|
8
|
-
assert.equal(parseBuildPlacement(" "), "
|
|
9
|
-
assert.equal(parseBuildPlacement("unknown"), "
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it("accepts here and new aliases", () => {
|
|
6
|
+
it("stays in this session by default", () => {
|
|
7
|
+
assert.equal(parseBuildPlacement(""), "here");
|
|
8
|
+
assert.equal(parseBuildPlacement(" "), "here");
|
|
9
|
+
assert.equal(parseBuildPlacement("unknown"), "here");
|
|
13
10
|
assert.equal(parseBuildPlacement("here"), "here");
|
|
14
11
|
assert.equal(parseBuildPlacement(" --here extra"), "here");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("accepts new aliases", () => {
|
|
15
|
+
assert.equal(parseBuildPlacement("new"), "new");
|
|
15
16
|
assert.equal(parseBuildPlacement("NEW"), "new");
|
|
16
17
|
assert.equal(parseBuildPlacement("--new"), "new");
|
|
17
18
|
});
|
|
18
19
|
});
|
|
20
|
+
|
|
21
|
+
describe("parseGoalMode", () => {
|
|
22
|
+
it("runs remaining tasks in this session by default", () => {
|
|
23
|
+
assert.equal(parseGoalMode(""), "all-here");
|
|
24
|
+
assert.equal(parseGoalMode(" "), "all-here");
|
|
25
|
+
assert.equal(parseGoalMode("unknown"), "all-here");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("accepts new and continue aliases", () => {
|
|
29
|
+
assert.equal(parseGoalMode("new"), "all-new");
|
|
30
|
+
assert.equal(parseGoalMode(" --new extra"), "all-new");
|
|
31
|
+
assert.equal(parseGoalMode("NEW"), "all-new");
|
|
32
|
+
assert.equal(parseGoalMode("continue"), "chain-here");
|
|
33
|
+
assert.equal(parseGoalMode("--continue"), "chain-here");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
export type BuildPlacement = "
|
|
1
|
+
export type BuildPlacement = "here" | "new";
|
|
2
|
+
export type GoalMode = "all-here" | "all-new" | "chain-here";
|
|
2
3
|
|
|
3
4
|
export function parseBuildPlacement(args: string): BuildPlacement {
|
|
4
5
|
const token = args.trim().split(/\s+/)[0]?.toLowerCase() ?? "";
|
|
5
|
-
if (token === "here" || token === "--here") return "here";
|
|
6
6
|
if (token === "new" || token === "--new") return "new";
|
|
7
|
-
return "
|
|
7
|
+
return "here";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function parseGoalMode(args: string): GoalMode {
|
|
11
|
+
const token = args.trim().split(/\s+/)[0]?.toLowerCase() ?? "";
|
|
12
|
+
if (token === "new" || token === "--new") return "all-new";
|
|
13
|
+
if (token === "continue" || token === "--continue") return "chain-here";
|
|
14
|
+
return "all-here";
|
|
8
15
|
}
|
package/extensions/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { matchesKey, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
9
9
|
import { Type } from "typebox";
|
|
10
10
|
import { isSafePlanCommand } from "./bash-guard.ts";
|
|
11
|
-
import { parseBuildPlacement, type BuildPlacement } from "./build-session.ts";
|
|
11
|
+
import { parseBuildPlacement, parseGoalMode, type BuildPlacement } from "./build-session.ts";
|
|
12
12
|
import { ensureDefaultGlobalConfig, loadConfig } from "./config.ts";
|
|
13
13
|
import {
|
|
14
14
|
ensurePlanDir,
|
|
@@ -38,21 +38,14 @@ type Mode = "idle" | "plan" | "build";
|
|
|
38
38
|
const CONTINUE_THIS = "Continue in this session";
|
|
39
39
|
const CONTINUE_NEW = "Continue in a new session";
|
|
40
40
|
const BUILD_HERE_COMMAND = "/build here";
|
|
41
|
+
const GOAL_CONTINUE_COMMAND = "/goal continue";
|
|
41
42
|
|
|
42
|
-
async function
|
|
43
|
-
if (!ctx.hasUI) return "here";
|
|
44
|
-
const choice = await ctx.ui.select("Start this task where?", [CONTINUE_THIS, CONTINUE_NEW]);
|
|
45
|
-
if (choice === CONTINUE_THIS) return "here";
|
|
46
|
-
if (choice === CONTINUE_NEW) return "new";
|
|
47
|
-
return undefined;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function startBuildInNewSession(ctx: ExtensionCommandContext): Promise<void> {
|
|
43
|
+
async function startBuildInNewSession(ctx: ExtensionCommandContext, command = BUILD_HERE_COMMAND): Promise<void> {
|
|
51
44
|
const parentSession = ctx.sessionManager.getSessionFile();
|
|
52
45
|
const result = await ctx.newSession({
|
|
53
46
|
parentSession,
|
|
54
47
|
withSession: async (nextCtx) => {
|
|
55
|
-
await nextCtx.sendUserMessage(
|
|
48
|
+
await nextCtx.sendUserMessage(command, { expandPromptTemplates: true });
|
|
56
49
|
},
|
|
57
50
|
});
|
|
58
51
|
if (result.cancelled) {
|
|
@@ -138,6 +131,7 @@ export default async function planTaskExtension(pi: ExtensionAPI): Promise<void>
|
|
|
138
131
|
|
|
139
132
|
let mode: Mode = "idle";
|
|
140
133
|
let continueAll = false;
|
|
134
|
+
let continueNew = false;
|
|
141
135
|
let currentTaskId: number | undefined;
|
|
142
136
|
let toolsBeforePlan: string[] | undefined;
|
|
143
137
|
let awaitingChoice = false;
|
|
@@ -193,6 +187,7 @@ export default async function planTaskExtension(pi: ExtensionAPI): Promise<void>
|
|
|
193
187
|
async function enterPlanMode(ctx: ExtensionContext): Promise<void> {
|
|
194
188
|
mode = "plan";
|
|
195
189
|
continueAll = false;
|
|
190
|
+
continueNew = false;
|
|
196
191
|
currentTaskId = undefined;
|
|
197
192
|
planReadyNotified = false;
|
|
198
193
|
resetFraming();
|
|
@@ -212,6 +207,7 @@ export default async function planTaskExtension(pi: ExtensionAPI): Promise<void>
|
|
|
212
207
|
function leaveModes(ctx: ExtensionContext): void {
|
|
213
208
|
mode = "idle";
|
|
214
209
|
continueAll = false;
|
|
210
|
+
continueNew = false;
|
|
215
211
|
currentTaskId = undefined;
|
|
216
212
|
resetFraming();
|
|
217
213
|
restoreTools();
|
|
@@ -263,6 +259,15 @@ export default async function planTaskExtension(pi: ExtensionAPI): Promise<void>
|
|
|
263
259
|
await startNextTask(ctx);
|
|
264
260
|
return;
|
|
265
261
|
}
|
|
262
|
+
if (continueNew) {
|
|
263
|
+
mode = "idle";
|
|
264
|
+
currentTaskId = undefined;
|
|
265
|
+
resetFraming();
|
|
266
|
+
updateStatus(ctx);
|
|
267
|
+
ctx.ui.notify("Task complete. Opening a new session for the next task.", "info");
|
|
268
|
+
pi.sendUserMessage("/goal-next-session", { expandPromptTemplates: true });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
266
271
|
if (!ctx.hasUI) {
|
|
267
272
|
ctx.ui.notify("Task complete. Run /build for the next task.", "info");
|
|
268
273
|
mode = "idle";
|
|
@@ -369,8 +374,11 @@ export default async function planTaskExtension(pi: ExtensionAPI): Promise<void>
|
|
|
369
374
|
async function beginBuild(
|
|
370
375
|
ctx: ExtensionCommandContext,
|
|
371
376
|
runAll: boolean,
|
|
372
|
-
placement: BuildPlacement = "
|
|
377
|
+
placement: BuildPlacement = "here",
|
|
378
|
+
options: { chainNew?: boolean; forkCommand?: string } = {},
|
|
373
379
|
): Promise<void> {
|
|
380
|
+
const chainNew = options.chainNew === true;
|
|
381
|
+
const forkCommand = options.forkCommand ?? BUILD_HERE_COMMAND;
|
|
374
382
|
const file = await loadTaskFile(ctx.cwd);
|
|
375
383
|
if (!file) {
|
|
376
384
|
ctx.ui.notify("No plan found. Run /plan first.", "error");
|
|
@@ -384,30 +392,43 @@ export default async function planTaskExtension(pi: ExtensionAPI): Promise<void>
|
|
|
384
392
|
ctx.ui.notify("All tasks are complete.", "info");
|
|
385
393
|
return;
|
|
386
394
|
}
|
|
387
|
-
if (!runAll) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (resolved === "new") {
|
|
391
|
-
await startBuildInNewSession(ctx);
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
395
|
+
if (!runAll && !chainNew && placement === "new") {
|
|
396
|
+
await startBuildInNewSession(ctx, forkCommand);
|
|
397
|
+
return;
|
|
394
398
|
}
|
|
395
399
|
continueAll = runAll;
|
|
400
|
+
continueNew = chainNew;
|
|
396
401
|
await enterBuildMode(ctx);
|
|
397
|
-
ctx.ui.notify(
|
|
402
|
+
ctx.ui.notify(
|
|
403
|
+
runAll
|
|
404
|
+
? "Building remaining tasks."
|
|
405
|
+
: chainNew
|
|
406
|
+
? "Building the next task. Remaining tasks will each start in a new session."
|
|
407
|
+
: "Building the next task.",
|
|
408
|
+
"info",
|
|
409
|
+
);
|
|
398
410
|
await startNextTask(ctx);
|
|
399
411
|
}
|
|
400
412
|
|
|
401
413
|
pi.registerCommand("build", {
|
|
402
|
-
description: "
|
|
414
|
+
description: "Execute the next planned task in this session. Use /build new to start it in a new session",
|
|
403
415
|
handler: async (args, ctx) => {
|
|
404
416
|
await beginBuild(ctx, false, parseBuildPlacement(args));
|
|
405
417
|
},
|
|
406
418
|
});
|
|
407
419
|
|
|
408
420
|
pi.registerCommand("goal", {
|
|
409
|
-
description: "Execute remaining planned tasks until
|
|
410
|
-
handler: async (
|
|
421
|
+
description: "Execute remaining planned tasks until complete. Use /goal new for one new session per task",
|
|
422
|
+
handler: async (args, ctx) => {
|
|
423
|
+
const goalMode = parseGoalMode(args);
|
|
424
|
+
if (goalMode === "all-new") {
|
|
425
|
+
await beginBuild(ctx, false, "new", { forkCommand: GOAL_CONTINUE_COMMAND });
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (goalMode === "chain-here") {
|
|
429
|
+
await beginBuild(ctx, false, "here", { chainNew: true });
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
411
432
|
await beginBuild(ctx, true);
|
|
412
433
|
},
|
|
413
434
|
});
|
|
@@ -432,9 +453,17 @@ export default async function planTaskExtension(pi: ExtensionAPI): Promise<void>
|
|
|
432
453
|
},
|
|
433
454
|
});
|
|
434
455
|
|
|
456
|
+
pi.registerCommand("goal-next-session", {
|
|
457
|
+
description: "Continue the next planned task in a new session, then keep chaining new sessions",
|
|
458
|
+
handler: async (_args, ctx) => {
|
|
459
|
+
await startBuildInNewSession(ctx, GOAL_CONTINUE_COMMAND);
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
|
|
435
463
|
pi.on("session_start", async (_event, ctx) => {
|
|
436
464
|
mode = "idle";
|
|
437
465
|
continueAll = false;
|
|
466
|
+
continueNew = false;
|
|
438
467
|
currentTaskId = undefined;
|
|
439
468
|
toolsBeforePlan = undefined;
|
|
440
469
|
awaitingChoice = false;
|