@ilya-lesikov/pi-pi 0.4.0 → 0.5.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.
@@ -72,15 +72,15 @@ export async function transitionToNextPhase(
72
72
  }
73
73
 
74
74
  orchestrator.updateStatus(ctx);
75
+ orchestrator.phaseCompactionSummary = `Phase "${currentPhase}" completed. Now entering "${next}" phase.`;
75
76
 
76
77
  if (next === "plan") {
77
78
  orchestrator.active.state.step = "await_planners";
78
79
  saveTask(orchestrator.active.dir, orchestrator.active.state);
79
80
  }
80
81
 
81
- orchestrator.compactAndTransition(ctx, orchestrator.active.dir, orchestrator.active.state.phase);
82
-
83
- if (next === "plan") {
82
+ const onReady = next === "plan" ? () => {
83
+ if (!orchestrator.active) return;
84
84
  orchestrator.pendingSubagentSpawns = Object.values(orchestrator.config.planners).filter((v) => v.enabled).length;
85
85
  orchestrator.failedPlannerVariants = [];
86
86
  spawnPlanners(orchestrator.pi, orchestrator.cwd, orchestrator.active.dir, orchestrator.active.taskId, orchestrator.config).then((result) => {
@@ -94,7 +94,9 @@ export async function transitionToNextPhase(
94
94
  orchestrator.pendingSubagentSpawns = 0;
95
95
  console.error(`[pi-pi] spawnPlanners failed: ${err.message}`);
96
96
  });
97
- }
97
+ } : undefined;
98
+
99
+ orchestrator.compactAndTransition(ctx, orchestrator.active.dir, orchestrator.active.state.phase, onReady);
98
100
 
99
101
  return { ok: true };
100
102
  }
@@ -109,7 +111,7 @@ export function registerCommandHandlers(orchestrator: Orchestrator): void {
109
111
  const { showPpMenu } = await import("./pp-menu.js");
110
112
  const text = await showPpMenu(orchestrator, ctx, "command");
111
113
  if (text) {
112
- pi.sendUserMessage(`[PI-PI] ${text}`, { deliverAs: "followUp" });
114
+ orchestrator.safeSendUserMessage(`[PI-PI] ${text}`);
113
115
  }
114
116
  },
115
117
  });
@@ -94,7 +94,7 @@ function tryCompleteReviewCycle(orchestrator: Orchestrator): void {
94
94
  },
95
95
  { deliverAs: "followUp" },
96
96
  );
97
- pi.sendUserMessage("[PI-PI] Review cycle is ready for apply_feedback. Read reviewer outputs and proceed.", { deliverAs: "followUp" });
97
+ orchestrator.safeSendUserMessage("[PI-PI] Review cycle is ready for apply_feedback. Read reviewer outputs and proceed.");
98
98
  }
99
99
 
100
100
  export async function enterReviewCycle(orchestrator: Orchestrator, ctx: any, kind: "auto" | "auto-deep" | "plannotator") {
@@ -204,13 +204,12 @@ export async function enterReviewCycle(orchestrator: Orchestrator, ctx: any, kin
204
204
  export async function stopTask(orchestrator: Orchestrator): Promise<string> {
205
205
  if (!orchestrator.active) return "No active task.";
206
206
  orchestrator.abortAllSubagents();
207
- orchestrator.active.state.phase = "done";
208
207
  saveTask(orchestrator.active.dir, orchestrator.active.state);
209
208
  const desc = orchestrator.active.description;
210
209
  await orchestrator.cleanupActive();
211
210
  const taskStore = (globalThis as any)[Symbol.for("pi-tasks:store")];
212
211
  taskStore?.clearAll?.();
213
- return `Task "${desc}" stopped.`;
212
+ return `Task "${desc}" stopped. Use /pp → Resume to continue.`;
214
213
  }
215
214
 
216
215
  export function finalizeReviewCycle(task: ActiveTask): void {
@@ -311,8 +310,8 @@ function registerSpecifyReviewsTool(orchestrator: Orchestrator): void {
311
310
  const { showActiveTaskMenu } = await import("./pp-menu.js");
312
311
  const text = await showActiveTaskMenu(orchestrator, ctx, `Plannotator review complete.\n\n${summary}`, "tool");
313
312
  if (orchestrator.phaseCompactionPending || orchestrator.taskDoneCompactionPending) {
314
- await orchestrator.waitForCompaction();
315
- return { content: [{ type: "text" as const, text: "Phase transition complete." }], details: {} };
313
+ ctx.abort?.();
314
+ return { content: [{ type: "text" as const, text: "" }], details: {} };
316
315
  }
317
316
  if (!text) {
318
317
  return { content: [{ type: "text" as const, text: "User dismissed the menu. Wait for the user's next message. When you resume work, update USER_REQUEST.md and RESEARCH.md with any new findings before calling pp_phase_complete." }], details: {} };
@@ -409,8 +408,8 @@ function registerPhaseCompleteTool(orchestrator: Orchestrator): void {
409
408
  const { showActiveTaskMenu } = await import("./pp-menu.js");
410
409
  const text = await showActiveTaskMenu(orchestrator, ctx, params.summary, "tool");
411
410
  if (orchestrator.phaseCompactionPending || orchestrator.taskDoneCompactionPending) {
412
- await orchestrator.waitForCompaction();
413
- return { content: [{ type: "text" as const, text: "Phase transition complete." }], details: {} };
411
+ ctx.abort?.();
412
+ return { content: [{ type: "text" as const, text: "" }], details: {} };
414
413
  }
415
414
  if (!text) {
416
415
  return { content: [{ type: "text" as const, text: "User dismissed the menu. Wait for the user's next message. When you resume work, update USER_REQUEST.md and RESEARCH.md with any new findings before calling pp_phase_complete." }], details: {} };
@@ -545,7 +544,8 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
545
544
  orchestrator.active.state.phase !== "plan" ||
546
545
  orchestrator.active.state.step !== "await_planners" ||
547
546
  orchestrator.spawnedAgentIds.size > 0 ||
548
- orchestrator.pendingSubagentSpawns > 0
547
+ orchestrator.pendingSubagentSpawns > 0 ||
548
+ orchestrator.phaseCompactionPending
549
549
  ) return;
550
550
 
551
551
  const plansDir = join(orchestrator.active.dir, "plans");
@@ -594,13 +594,13 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
594
594
  console.error(`[pi-pi] retry spawnPlanners failed: ${err.message}`);
595
595
  checkPlannerCompletion();
596
596
  });
597
- pi.sendUserMessage(`[PI-PI] Retrying failed planners: ${variantsText}.`, { deliverAs: "followUp" });
597
+ orchestrator.safeSendUserMessage(`[PI-PI] Retrying failed planners: ${variantsText}.`);
598
598
  return;
599
599
  }
600
600
  }
601
601
 
602
602
  if (choice === "Stop task") {
603
- pi.sendUserMessage(`[PI-PI] ${await stopTask(orchestrator)}`, { deliverAs: "followUp" });
603
+ orchestrator.safeSendUserMessage(`[PI-PI] ${await stopTask(orchestrator)}`);
604
604
  return;
605
605
  }
606
606
 
@@ -632,7 +632,7 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
632
632
  orchestrator.failedPlannerVariants = [];
633
633
  orchestrator.active.state.step = "synthesize";
634
634
  saveTask(orchestrator.active.dir, orchestrator.active.state);
635
- pi.sendUserMessage("[PI-PI] All planners completed. Read their outputs and synthesize the plan.", { deliverAs: "followUp" });
635
+ orchestrator.safeSendUserMessage("[PI-PI] All planners completed. Read their outputs and synthesize the plan.");
636
636
  }
637
637
 
638
638
  function checkReviewCycleCompletion(): void {
@@ -640,7 +640,8 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
640
640
  !orchestrator.active?.state.reviewCycle ||
641
641
  orchestrator.active.state.reviewCycle.step !== "await_reviewers" ||
642
642
  orchestrator.spawnedAgentIds.size > 0 ||
643
- orchestrator.pendingSubagentSpawns > 0
643
+ orchestrator.pendingSubagentSpawns > 0 ||
644
+ orchestrator.phaseCompactionPending
644
645
  ) return;
645
646
 
646
647
  const failedReviewerVariants = [...orchestrator.failedReviewerVariants];
@@ -705,14 +706,14 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
705
706
  console.error(`[pi-pi] retry spawn reviewers failed (${phase}): ${err.message}`);
706
707
  checkReviewCycleCompletion();
707
708
  });
708
- pi.sendUserMessage(`[PI-PI] Retrying failed reviewers: ${variantsText}.`, { deliverAs: "followUp" });
709
+ orchestrator.safeSendUserMessage(`[PI-PI] Retrying failed reviewers: ${variantsText}.`);
709
710
  return;
710
711
  }
711
712
  }
712
713
  }
713
714
 
714
715
  if (choice === "Stop task") {
715
- pi.sendUserMessage(`[PI-PI] ${await stopTask(orchestrator)}`, { deliverAs: "followUp" });
716
+ orchestrator.safeSendUserMessage(`[PI-PI] ${await stopTask(orchestrator)}`);
716
717
  return;
717
718
  }
718
719
 
@@ -722,7 +723,7 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
722
723
  orchestrator.active.state.reviewCycle = null;
723
724
  orchestrator.active.state.step = "user_gate";
724
725
  saveTask(orchestrator.active.dir, orchestrator.active.state);
725
- pi.sendUserMessage("[PI-PI] Review cycle skipped. Use /pp to choose the next action.", { deliverAs: "followUp" });
726
+ orchestrator.safeSendUserMessage("[PI-PI] Review cycle skipped. Use /pp to choose the next action.");
726
727
  return;
727
728
  }
728
729
 
@@ -1099,9 +1100,11 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
1099
1100
  if (!orchestrator.active || orchestrator.active.state.phase === "done") return;
1100
1101
 
1101
1102
  if (orchestrator.phaseCompactionPending) {
1103
+ const summary = orchestrator.phaseCompactionSummary || "Phase transition in progress.";
1104
+ orchestrator.phaseCompactionSummary = "";
1102
1105
  return {
1103
1106
  compaction: {
1104
- summary: `Previous phase (${orchestrator.active.state.phase}) completed. Transitioning to next phase.`,
1107
+ summary,
1105
1108
  firstKeptEntryId: event.preparation.firstKeptEntryId,
1106
1109
  tokensBefore: event.preparation.tokensBefore,
1107
1110
  },
@@ -1143,6 +1146,7 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
1143
1146
  }
1144
1147
 
1145
1148
  if (!orchestrator.active || orchestrator.active.state.phase === "done") return;
1149
+ if (orchestrator.phaseCompactionPending || orchestrator.taskDoneCompactionPending) return;
1146
1150
  orchestrator.updateStatus(ctx);
1147
1151
 
1148
1152
  if (
@@ -1186,7 +1190,7 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
1186
1190
  orchestrator.pendingRetryTimer = setTimeout(() => {
1187
1191
  orchestrator.pendingRetryTimer = null;
1188
1192
  if (orchestrator.activeTaskToken !== taskToken || !orchestrator.active) return;
1189
- pi.sendUserMessage(`[PI-PI] Previous request failed due to an API error. Continue working on the current phase (${phase}).`, { deliverAs: "followUp" });
1193
+ orchestrator.safeSendUserMessage(`[PI-PI] Previous request failed due to an API error. Continue working on the current phase (${phase}).`);
1190
1194
  }, delay);
1191
1195
  } else {
1192
1196
  ctx.ui.notify(`API error persisted after 3 retries: ${errorMsg}. Stopping auto-retry.`, "error");
@@ -1208,6 +1212,7 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
1208
1212
  orchestrator.awaitPollTimer = null;
1209
1213
  return;
1210
1214
  }
1215
+ if (orchestrator.phaseCompactionPending) return;
1211
1216
  const taskDir = orchestrator.active.dir;
1212
1217
  if (orchestrator.active.state.step === "await_planners") {
1213
1218
  const plansDir = join(taskDir, "plans");
@@ -1221,7 +1226,7 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
1221
1226
  orchestrator.pendingSubagentSpawns = 0;
1222
1227
  orchestrator.active.state.step = "synthesize";
1223
1228
  saveTask(orchestrator.active.dir, orchestrator.active.state);
1224
- pi.sendUserMessage("[PI-PI] All planners completed. Read their outputs and synthesize the plan.", { deliverAs: "followUp" });
1229
+ orchestrator.safeSendUserMessage("[PI-PI] All planners completed. Read their outputs and synthesize the plan.");
1225
1230
  }
1226
1231
  }
1227
1232
  } else if (orchestrator.active.state.step === "await_reviewers" && orchestrator.active.state.reviewCycle) {
@@ -1252,7 +1257,7 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
1252
1257
  { customType: "pp-review-ready", content: `[PI-PI] Reviewer outputs are ready.\n\n${rendered}`, display: false },
1253
1258
  { deliverAs: "followUp" },
1254
1259
  );
1255
- pi.sendUserMessage("[PI-PI] Review cycle is ready for apply_feedback. Read reviewer outputs and proceed.", { deliverAs: "followUp" });
1260
+ orchestrator.safeSendUserMessage("[PI-PI] Review cycle is ready for apply_feedback. Read reviewer outputs and proceed.");
1256
1261
  }
1257
1262
  } else {
1258
1263
  clearInterval(orchestrator.awaitPollTimer!);
@@ -1281,19 +1286,16 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
1281
1286
  const endsWithText = lastPart?.type === "text" && lastPart?.text?.trim();
1282
1287
  if (hasText && (!hasToolCalls || endsWithText)) {
1283
1288
  const step = orchestrator.active.state.step;
1284
- if (step !== "await_planners" && step !== "await_reviewers") {
1285
- pi.sendMessage(
1286
- {
1287
- customType: "pp-phase-complete-reminder",
1288
- content: `[PI-PI] You stopped without calling pp_phase_complete. If you are done with the ${phase} phase, call pp_phase_complete now. If not, continue working.`,
1289
- display: false,
1290
- },
1291
- { deliverAs: "followUp" },
1292
- );
1289
+ if (step !== "await_planners" && step !== "await_reviewers" && !orchestrator.textStopReminderSent) {
1290
+ orchestrator.textStopReminderSent = true;
1291
+ orchestrator.safeSendUserMessage(`[PI-PI] You stopped without calling pp_phase_complete. If you are done with the ${phase} phase, call pp_phase_complete now. If not, continue working.`);
1293
1292
  }
1293
+ } else {
1294
+ orchestrator.textStopReminderSent = false;
1294
1295
  }
1295
1296
  return;
1296
1297
  }
1298
+ orchestrator.textStopReminderSent = false;
1297
1299
  if (orchestrator.nudgeHalted) return;
1298
1300
  if (orchestrator.spawnedAgentIds.size > 0 || orchestrator.pendingSubagentSpawns > 0) return;
1299
1301
 
@@ -1306,14 +1308,7 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
1306
1308
  orchestrator.nudgeTimestamps = orchestrator.nudgeTimestamps.filter((t) => now - t < 60000);
1307
1309
 
1308
1310
  const sendNudge = () => {
1309
- pi.sendMessage(
1310
- {
1311
- customType: "pp-continuation",
1312
- content: `[PI-PI] Your previous response was interrupted. Continue working on the current phase (${phase}). Pick up where you left off.`,
1313
- display: false,
1314
- },
1315
- { deliverAs: "followUp" },
1316
- );
1311
+ orchestrator.safeSendUserMessage(`[PI-PI] Your previous response was interrupted. Continue working on the current phase (${phase}). Pick up where you left off.`);
1317
1312
  };
1318
1313
 
1319
1314
  if (orchestrator.nudgeTimestamps.length <= 3) {
@@ -7,6 +7,7 @@ import {
7
7
  loadTask,
8
8
  saveTask,
9
9
  lockTask,
10
+ appendTaskLog,
10
11
  type TaskType,
11
12
  type TaskState,
12
13
  type Phase,
@@ -60,7 +61,7 @@ export class Orchestrator {
60
61
  }>();
61
62
  staleAgentTimer: ReturnType<typeof setInterval> | null = null;
62
63
  phaseCompactionPending = false;
63
- phaseCompactionResolve: (() => void) | null = null;
64
+ phaseCompactionSummary = "";
64
65
  taskDoneCompactionPending = false;
65
66
  taskDoneCompactionSummary = "";
66
67
  nudgeTimestamps: number[] = [];
@@ -69,6 +70,7 @@ export class Orchestrator {
69
70
  pendingSubagentSpawns = 0;
70
71
  errorRetryCount = 0;
71
72
  commitReminderSent = false;
73
+ textStopReminderSent = false;
72
74
  phaseStartTime = 0;
73
75
  awaitPollTimer: ReturnType<typeof setInterval> | null = null;
74
76
  pendingRetryTimer: ReturnType<typeof setTimeout> | null = null;
@@ -86,6 +88,25 @@ export class Orchestrator {
86
88
 
87
89
  constructor(readonly pi: ExtensionAPI) {}
88
90
 
91
+ safeSendUserMessage(text: string): void {
92
+ const log = (event: string, extra?: Record<string, unknown>) => {
93
+ if (this.active) appendTaskLog(this.active.dir, "debug.jsonl", { timestamp: new Date().toISOString(), event, text: text.slice(0, 200), ...extra });
94
+ };
95
+ const attempt = (retries: number) => {
96
+ try {
97
+ this.pi.sendUserMessage(text);
98
+ log("safeSend_sent", { retries });
99
+ } catch (err: any) {
100
+ if (retries < 30) {
101
+ setTimeout(() => attempt(retries + 1), 1000);
102
+ } else {
103
+ log("safeSend_failed", { error: err?.message ?? String(err), retries });
104
+ }
105
+ }
106
+ };
107
+ attempt(0);
108
+ }
109
+
89
110
  truncateResult(result: string): string {
90
111
  const trimmed = result.trim();
91
112
  if (!trimmed) return "";
@@ -247,11 +268,10 @@ export class Orchestrator {
247
268
  ): Promise<void> {
248
269
  if (this.active) {
249
270
  ctx.ui.notify(
250
- `Finishing previous task "${this.active.description}" (phase: ${this.active.state.phase})…`,
271
+ `Pausing previous task "${this.active.description}" (phase: ${this.active.state.phase})…`,
251
272
  "info",
252
273
  );
253
274
  this.abortAllSubagents();
254
- this.active.state.phase = "done";
255
275
  saveTask(this.active.dir, this.active.state);
256
276
  unregisterAgentDefinitions(this.pi);
257
277
  await this.cleanupActive();
@@ -344,7 +364,7 @@ export class Orchestrator {
344
364
  } else if (isWaitingForPlanners) {
345
365
  ctx.ui.notify("Entered plan phase. Waiting for planners to complete before synthesis.", "info");
346
366
  } else {
347
- this.pi.sendUserMessage(`[PI-PI] Entered ${this.active.state.phase} phase. Begin working.`, { deliverAs: "followUp" });
367
+ this.safeSendUserMessage(`[PI-PI] Entered ${this.active.state.phase} phase. Begin working.`);
348
368
  }
349
369
 
350
370
  if (this.active.state.phase === "plan" && this.active.state.step === "await_planners") {
@@ -383,11 +403,12 @@ export class Orchestrator {
383
403
  this.pendingSubagentSpawns = 0;
384
404
  this.errorRetryCount = 0;
385
405
  this.commitReminderSent = false;
406
+ this.textStopReminderSent = false;
386
407
  this.nudgeTimestamps = [];
387
408
  this.cooldownHits = [];
388
409
  this.nudgeHalted = false;
389
410
  this.phaseCompactionPending = false;
390
- this.phaseCompactionResolve = null;
411
+ this.phaseCompactionSummary = "";
391
412
  this.phaseStartTime = 0;
392
413
  this.userGatePending = false;
393
414
  this.reviewTransitionToken = -1;
@@ -458,53 +479,32 @@ export class Orchestrator {
458
479
  }
459
480
  }
460
481
 
461
- waitForCompaction(): Promise<void> {
462
- if (!this.phaseCompactionPending && !this.taskDoneCompactionPending) return Promise.resolve();
463
- return new Promise((resolve) => {
464
- this.phaseCompactionResolve = resolve;
465
- });
466
- }
467
-
468
- compactAndTransition(ctx: ExtensionContext, taskDir: string, phase: Phase): void {
482
+ compactAndTransition(ctx: ExtensionContext, taskDir: string, phase: Phase, onReady?: () => void): void {
469
483
  this.phaseCompactionPending = true;
484
+ const finalize = () => {
485
+ this.phaseStartTime = Date.now();
486
+ if (this.active && (phase === "plan" || phase === "implement")) {
487
+ const modelConfig = this.config.mainModel.implement;
488
+ this.switchModel(ctx, modelConfig.model, modelConfig.thinking).catch(() => {});
489
+ }
490
+ this.injectContextAndArtifacts(taskDir, phase);
491
+ onReady?.();
492
+ if (this.active?.state.phase === "plan" && this.active.state.step === "await_planners") {
493
+ ctx.ui.notify("Entered plan phase. Waiting for planners to complete before synthesis.", "info");
494
+ } else {
495
+ this.safeSendUserMessage(`[PI-PI] Entered ${phase} phase. Begin working.`);
496
+ }
497
+ };
470
498
  ctx.compact({
471
499
  customInstructions: "Phase transition — discard all prior conversation. Produce a one-line summary: 'Previous phase completed.'",
472
500
  onComplete: () => {
473
501
  this.phaseCompactionPending = false;
474
- if (this.phaseCompactionResolve) {
475
- this.phaseCompactionResolve();
476
- this.phaseCompactionResolve = null;
477
- }
478
- this.phaseStartTime = Date.now();
479
- if (this.active && (phase === "plan" || phase === "implement")) {
480
- const modelConfig = this.config.mainModel.implement;
481
- this.switchModel(ctx, modelConfig.model, modelConfig.thinking).catch(() => {});
482
- }
483
- this.injectContextAndArtifacts(taskDir, phase);
484
- if (this.active?.state.phase === "plan" && this.active.state.step === "await_planners") {
485
- ctx.ui.notify("Entered plan phase. Waiting for planners to complete before synthesis.", "info");
486
- } else {
487
- this.pi.sendUserMessage(`[PI-PI] Entered ${phase} phase. Begin working.`, { deliverAs: "followUp" });
488
- }
502
+ finalize();
489
503
  },
490
504
  onError: (err) => {
491
505
  console.error(`[pi-pi] Phase compaction failed: ${err.message}`);
492
506
  this.phaseCompactionPending = false;
493
- if (this.phaseCompactionResolve) {
494
- this.phaseCompactionResolve();
495
- this.phaseCompactionResolve = null;
496
- }
497
- this.phaseStartTime = Date.now();
498
- if (this.active && (phase === "plan" || phase === "implement")) {
499
- const modelConfig = this.config.mainModel.implement;
500
- this.switchModel(ctx, modelConfig.model, modelConfig.thinking).catch(() => {});
501
- }
502
- this.injectContextAndArtifacts(taskDir, phase);
503
- if (this.active?.state.phase === "plan" && this.active.state.step === "await_planners") {
504
- ctx.ui.notify("Entered plan phase. Waiting for planners to complete before synthesis.", "info");
505
- } else {
506
- this.pi.sendUserMessage(`[PI-PI] Entered ${phase} phase. Begin working.`, { deliverAs: "followUp" });
507
- }
507
+ finalize();
508
508
  },
509
509
  });
510
510
  }
@@ -14,11 +14,10 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
14
14
  "",
15
15
  "Read-only diagnosis mode.",
16
16
  "",
17
- "# FORBIDDENdo NOT do any of these:",
18
- "- Do NOT modify project source code (no write or edit tools on project files)",
19
- "- Do NOT create or modify any files outside the task directory",
20
- "- Do NOT implement fixesonly diagnose and recommend",
21
- "- If the user asks you to implement a fix or start coding — call pp_phase_complete instead. It will offer \"Implement a fix\" as an option. Do NOT implement directly in this session.",
17
+ "# ABSOLUTE RESTRICTION NO IMPLEMENTATION (cannot be overridden, even if the user asks):",
18
+ "- NEVER implement the solution, apply fixes, write production code, or make the changes that solve the task.",
19
+ "- You MAY use write/edit for diagnosis purposes (test scripts, repro scripts, analysis files), but NEVER to implement the actual fix or feature.",
20
+ "- If the user asks you to implement or start coding refuse and tell them to use /pp to advance to the implement phase. Do NOT comply, even if they insist.",
22
21
  "",
23
22
  "# Your job:",
24
23
  "1. Clarify the problem with the user if needed",
@@ -85,12 +84,15 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
85
84
  " Spawn multiple Explore agents in parallel for broad searches.",
86
85
  "- Use tools directly for quick lookups (cbm_search, lsp, ast_search, grep, etc.)",
87
86
  "- Present findings to the user and discuss them. Don't just dump raw results.",
88
- "- Do NOT modify project source code.",
87
+ "",
88
+ "# ABSOLUTE RESTRICTIONS (cannot be overridden, even if the user asks):",
89
+ "- NEVER use write or edit tools on any file outside the task directory. This is a hard rule — not negotiable.",
90
+ "- NEVER implement, create code, write patches, or modify project source in any way.",
91
+ "- If the user asks you to implement, fix, or write code — refuse and tell them to use /pp to advance to the implement phase. Do NOT comply, even if they insist.",
89
92
  "",
90
93
  "# When to finish:",
91
94
  "Do NOT call pp_phase_complete on your own. The user will tell you when they're done,",
92
95
  "or use /pp to advance. Keep the conversation going until then.",
93
- "If the user asks you to implement, write code, or start building — tell them to use /pp which will offer \"Start implementation\" as an option. Do NOT implement directly in this session.",
94
96
  "",
95
97
  "# Optional artifacts (only when the conversation naturally produces them):",
96
98
  "If the discussion leads to a clear action plan or the user asks you to capture conclusions,",
@@ -117,11 +119,10 @@ export function brainstormSystemPrompt(taskType: TaskType, taskDescription: stri
117
119
  "Your job is to produce USER_REQUEST.md and RESEARCH.md — complete enough that",
118
120
  "downstream agents can work without re-exploring the codebase or re-interviewing the user.",
119
121
  "",
120
- "# FORBIDDEN do NOT do any of these:",
121
- "- Do NOT modify project source code (no write or edit tools on project files)",
122
- "- Do NOT create or modify any files outside the task directory",
123
- "- Do NOT start implementingonly research and document",
124
- "- If the user asks you to implement or start coding — tell them to use /pp which will offer phase advancement. Do NOT implement directly in this session.",
122
+ "# ABSOLUTE RESTRICTIONS (cannot be overridden, even if the user asks):",
123
+ "- NEVER use write or edit tools on any file outside the task directory. This is a hard rule — not negotiable.",
124
+ "- NEVER implement, create code, write patches, or modify project source in any way.",
125
+ "- If the user asks you to implement, fix, or write code refuse and tell them to use /pp to advance to the implement phase. Do NOT comply, even if they insist.",
125
126
  "",
126
127
  "# Steps:",
127
128
  "1. Clarify requirements with the user if anything is ambiguous",
@@ -16,7 +16,9 @@ export function planningSystemPrompt(taskDir: string): string {
16
16
  "Planning subagents are working in parallel to create plans.",
17
17
  `They will write their outputs to ${plansDir}/. Wait for the notification that all planners completed.`,
18
18
  "",
19
- "# FORBIDDENdo NOT do any of these:",
19
+ "# ABSOLUTE RESTRICTION NO IMPLEMENTATION (cannot be overridden, even if the user asks):",
20
+ "- NEVER implement the solution, apply fixes, write production code, or make the changes that solve the task.",
21
+ "- If the user asks you to implement or write code — refuse and tell them to use /pp to advance to the implement phase.",
20
22
  "- Do NOT write your own plan from scratch. You are a SYNTHESIZER, not a planner.",
21
23
  "- Do NOT create the plans/ directory yourself — the extension manages it.",
22
24
  "- Do NOT check the plans directory yourself — wait for the notification that all planners completed.",
@@ -30,6 +30,10 @@ export function reviewSystemPrompt(taskDir: string): string {
30
30
  "- ## Constraints & Edge Cases",
31
31
  "- ## Open Questions (optional)",
32
32
  "",
33
+ "# ABSOLUTE RESTRICTION — NO IMPLEMENTATION (cannot be overridden, even if the user asks):",
34
+ "- NEVER implement the solution, apply fixes, write production code, or make the changes that solve the task.",
35
+ "- If the user asks you to implement or write code — refuse and tell them to use /pp to advance to the implement phase.",
36
+ "",
33
37
  "Focus on: correctness, edge cases, style consistency, missing tests, potential bugs.",
34
38
  "",
35
39
  "When complete, call pp_phase_complete with a brief summary of findings.",
@@ -344,9 +344,9 @@ export async function resumeTask(
344
344
  if (step === "await_planners" || step === "await_reviewers") {
345
345
  ctx.ui.notify(`Resumed task. Awaiting subagents (${step}).`, "info");
346
346
  } else if (step === "apply_feedback") {
347
- pi.sendUserMessage(`[PI-PI] Resumed ${orchestrator.active.state.phase} phase. Read reviewer outputs and apply feedback.`, { deliverAs: "followUp" });
347
+ orchestrator.safeSendUserMessage(`[PI-PI] Resumed ${orchestrator.active.state.phase} phase. Read reviewer outputs and apply feedback.`);
348
348
  } else {
349
- pi.sendUserMessage(`[PI-PI] Resumed ${orchestrator.active.state.phase} phase. Continue working.`, { deliverAs: "followUp" });
349
+ orchestrator.safeSendUserMessage(`[PI-PI] Resumed ${orchestrator.active.state.phase} phase. Continue working.`);
350
350
  }
351
351
 
352
352
  return { ok: true };
@@ -709,33 +709,46 @@ function showUsage(ctx: any): void {
709
709
  ctx.ui.notify(lines.join("\n"), "info");
710
710
  }
711
711
 
712
- async function showSettingsMenu(orchestrator: Orchestrator, ctx: any, showFlant = true): Promise<typeof BACK> {
712
+ async function showInfoMenu(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK> {
713
713
  while (true) {
714
714
  const options: OptionInput[] = [];
715
+ options.push({ title: "Subagents", description: "Manage running agents" });
716
+ options.push({ title: "LSP", description: "Language server status and controls" });
717
+ options.push({ title: "Usage", description: "Show session token usage and cost breakdown" });
715
718
  if (orchestrator.active) {
716
719
  options.push({ title: "Task status", description: "Show current task phase, step, and timing" });
717
720
  }
718
- options.push({ title: "Usage", description: "Show session token usage and cost breakdown" });
719
- options.push({ title: "LSP", description: "Language server status and controls" });
720
- if (showFlant) {
721
- options.push({ title: "Flant AI Infrastructure", description: "Configure corporate AI model provider" });
722
- }
723
721
  options.push({ title: "Back", description: "Return to the previous menu" });
724
722
 
725
- const choice = await selectOption(ctx, "Settings", options);
723
+ const choice = await selectOption(ctx, "Info", options);
726
724
  if (!choice || choice === "Back") return BACK;
727
- if (choice === "Task status") {
728
- showStatus(orchestrator, ctx);
725
+ if (choice === "Subagents") {
726
+ await showSubagentsMenu(ctx);
727
+ continue;
728
+ }
729
+ if (choice === "LSP") {
730
+ await showLspMenu(ctx);
729
731
  continue;
730
732
  }
731
733
  if (choice === "Usage") {
732
734
  showUsage(ctx);
733
735
  continue;
734
736
  }
735
- if (choice === "LSP") {
736
- await showLspMenu(ctx);
737
+ if (choice === "Task status") {
738
+ showStatus(orchestrator, ctx);
737
739
  continue;
738
740
  }
741
+ }
742
+ }
743
+
744
+ async function showSettingsMenu(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK> {
745
+ while (true) {
746
+ const options: OptionInput[] = [];
747
+ options.push({ title: "Flant AI Infrastructure", description: "Configure corporate AI model provider" });
748
+ options.push({ title: "Back", description: "Return to the previous menu" });
749
+
750
+ const choice = await selectOption(ctx, "Settings", options);
751
+ if (!choice || choice === "Back") return BACK;
739
752
  await showFlantInfraMenu(orchestrator, ctx);
740
753
  }
741
754
  }
@@ -1021,56 +1034,72 @@ async function showTaskTypeMenu(
1021
1034
  }
1022
1035
  }
1023
1036
 
1024
- async function showNoActiveMenu(orchestrator: Orchestrator, ctx: any): Promise<string | undefined> {
1037
+ async function showTaskMenu(orchestrator: Orchestrator, ctx: any): Promise<typeof BACK | "started"> {
1025
1038
  while (true) {
1026
- const choice = await selectOption(ctx, "/pp", [
1039
+ const choice = await selectOption(ctx, "Task", [
1027
1040
  { title: "Debug", description: "Diagnose an issue. Then (optionally) fix it" },
1028
1041
  { title: "Brainstorm", description: "Explore and brainstorm. Then (optionally) plan and implement" },
1029
1042
  { title: "Implement", description: "Brainstorm, plan and implement" },
1030
1043
  { title: "Review", description: "Review code changes, diffs, or pull requests" },
1031
1044
  { title: "Resume", description: "Resume a previously unfinished task" },
1032
- { title: "Subagents", description: "Manage running agents" },
1033
- { title: "Settings", description: "LSP, Flant AI, and other configuration" },
1034
- { title: "Back", description: "Close this menu" },
1045
+ { title: "Back", description: "Return to the previous menu" },
1035
1046
  ]);
1036
- if (!choice || choice === "Back") return undefined;
1047
+ if (!choice || choice === "Back") return BACK;
1037
1048
 
1038
1049
  if (choice === "Debug") {
1039
1050
  const result = await showTaskTypeMenu(orchestrator, ctx, "debug", "Describe the task");
1040
- if (result === "started") return undefined;
1051
+ if (result === "started") return "started";
1041
1052
  continue;
1042
1053
  }
1043
1054
 
1044
1055
  if (choice === "Brainstorm") {
1045
1056
  const result = await showTaskTypeMenu(orchestrator, ctx, "brainstorm", "Describe the task");
1046
- if (result === "started") return undefined;
1057
+ if (result === "started") return "started";
1047
1058
  continue;
1048
1059
  }
1049
1060
 
1050
1061
  if (choice === "Implement") {
1051
1062
  const result = await showImplementMenu(orchestrator, ctx);
1052
- if (result === "started") return undefined;
1063
+ if (result === "started") return "started";
1053
1064
  continue;
1054
1065
  }
1055
1066
 
1056
1067
  if (choice === "Review") {
1057
1068
  const result = await showReviewMenu(orchestrator, ctx);
1058
- if (result === "started") return undefined;
1069
+ if (result === "started") return "started";
1059
1070
  continue;
1060
1071
  }
1061
1072
 
1062
1073
  if (choice === "Resume") {
1063
1074
  const result = await showResumeMenu(orchestrator, ctx, undefined, "No paused tasks found.");
1075
+ if (result === "started") return "started";
1076
+ continue;
1077
+ }
1078
+ }
1079
+ }
1080
+
1081
+ async function showNoActiveMenu(orchestrator: Orchestrator, ctx: any): Promise<string | undefined> {
1082
+ while (true) {
1083
+ const choice = await selectOption(ctx, "/pp", [
1084
+ { title: "Task", description: "Start a new task or resume a paused one" },
1085
+ { title: "Info", description: "Subagents, LSP, usage, and task status" },
1086
+ { title: "Settings", description: "Flant AI and other configuration" },
1087
+ { title: "Back", description: "Close this menu" },
1088
+ ]);
1089
+ if (!choice || choice === "Back") return undefined;
1090
+
1091
+ if (choice === "Task") {
1092
+ const result = await showTaskMenu(orchestrator, ctx);
1064
1093
  if (result === "started") return undefined;
1065
1094
  continue;
1066
1095
  }
1067
1096
 
1068
- if (choice === "Subagents") {
1069
- await showSubagentsMenu(ctx);
1097
+ if (choice === "Info") {
1098
+ await showInfoMenu(orchestrator, ctx);
1070
1099
  continue;
1071
1100
  }
1072
1101
 
1073
- await showSettingsMenu(orchestrator, ctx, true);
1102
+ await showSettingsMenu(orchestrator, ctx);
1074
1103
  }
1075
1104
  }
1076
1105
 
@@ -1126,12 +1155,12 @@ export async function showActiveTaskMenu(
1126
1155
  const opt = (title: string, description: string): OptionInput => ({ title, description });
1127
1156
 
1128
1157
  const options: OptionInput[] = [];
1129
- options.push(opt("Finish", "Complete, pause, or continue to next phase"));
1158
+ options.push(opt("Next", "Complete, pause, or continue to next phase"));
1130
1159
  if (!waiting) {
1131
1160
  options.push(opt("Review", "Auto review, Plannotator, or manual review"));
1132
1161
  }
1133
- options.push(opt("Subagents", "Manage running agents"));
1134
- options.push(opt("Settings", "Task status, usage, and LSP"));
1162
+ options.push(opt("Info", "Subagents, LSP, usage, and task status"));
1163
+ options.push(opt("Settings", "Flant AI and other configuration"));
1135
1164
  options.push(opt("Back", "Return to the prompt and keep working"));
1136
1165
 
1137
1166
  const headerLines = [`/pp\n\nTask: ${task.type}\nPhase: ${phase}`];
@@ -1142,12 +1171,12 @@ export async function showActiveTaskMenu(
1142
1171
  return "";
1143
1172
  }
1144
1173
 
1145
- if (choice === "Subagents") {
1146
- await showSubagentsMenu(ctx);
1174
+ if (choice === "Info") {
1175
+ await showInfoMenu(orchestrator, ctx);
1147
1176
  continue;
1148
1177
  }
1149
1178
  if (choice === "Settings") {
1150
- await showSettingsMenu(orchestrator, ctx, false);
1179
+ await showSettingsMenu(orchestrator, ctx);
1151
1180
  continue;
1152
1181
  }
1153
1182
 
@@ -1155,7 +1184,7 @@ export async function showActiveTaskMenu(
1155
1184
  await abortCurrentWork(orchestrator, ctx);
1156
1185
  }
1157
1186
 
1158
- if (choice === "Finish") {
1187
+ if (choice === "Next") {
1159
1188
  const canContinue = phase !== "implement" && !waiting;
1160
1189
  const continueLabel = phase === "plan" ? "Continue to implement" : "Continue to plan & implement";
1161
1190
  const finishOptions: OptionInput[] = [];
@@ -1166,7 +1195,7 @@ export async function showActiveTaskMenu(
1166
1195
  finishOptions.push(opt("Pause", "Suspend task to resume later"));
1167
1196
  finishOptions.push(opt("Back", "Return to the previous menu"));
1168
1197
 
1169
- const finishChoice = await selectOption(ctx, "Finish", finishOptions);
1198
+ const finishChoice = await selectOption(ctx, "Next", finishOptions);
1170
1199
  if (!finishChoice || finishChoice === "Back") continue;
1171
1200
  if (finishChoice === "Pause") {
1172
1201
  const text = await pauseTask(orchestrator, ctx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilya-lesikov/pi-pi",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Pi-Pi Coding Agent: based on Pi Coding Agent, but twice as good",
5
5
  "keywords": [
6
6
  "pi-package",