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 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` | Ask where to run, execute the next unfinished task, then ask whether to continue here or in a new session. |
33
- | `/build here` | Same as `/build`, but skip the first session prompt and stay in this session. |
34
- | `/build new` | Same as `/build`, but skip the first session prompt and start a new session. |
35
- | `/goal` | Execute remaining tasks until `task.md` is complete. No session prompts. |
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
- `/goal` never asks about sessions. After a `/build` task finishes, the continue-here / new-session prompt still appears. Those prompts use this package's `select` dialog, not `ask_user_question`.
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` needs:
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` and `/build` send a short visible user message to start the turn.
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` and `/build` keep juicesharp's `ask_user_question` available and tell the model to call it for consequential choices the repo cannot answer:
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` resumes from the first unchecked item.
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("asks by default", () => {
7
- assert.equal(parseBuildPlacement(""), "ask");
8
- assert.equal(parseBuildPlacement(" "), "ask");
9
- assert.equal(parseBuildPlacement("unknown"), "ask");
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 = "ask" | "here" | "new";
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 "ask";
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
  }
@@ -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 chooseBuildPlacement(ctx: ExtensionCommandContext): Promise<"here" | "new" | undefined> {
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(BUILD_HERE_COMMAND, { expandPromptTemplates: true });
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 = "ask",
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
- const resolved = placement === "ask" ? await chooseBuildPlacement(ctx) : placement;
389
- if (!resolved) return;
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(runAll ? "Building remaining tasks." : "Building the next task.", "info");
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: "Choose this session or a new session, then execute the next planned task",
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 the list is complete",
410
- handler: async (_args, ctx) => {
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-plan-task",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "license": "MIT",
5
5
  "description": "Pi package: /plan, /build, /goal, and /tasks.",
6
6
  "keywords": ["pi-package", "pi-extension", "plan", "build"],