@ilya-lesikov/pi-pi 0.8.0 → 0.9.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.
Files changed (33) hide show
  1. package/extensions/orchestrator/agents/code-reviewer.ts +21 -5
  2. package/extensions/orchestrator/agents/constraints.test.ts +39 -1
  3. package/extensions/orchestrator/agents/constraints.ts +63 -2
  4. package/extensions/orchestrator/agents/tool-routing.ts +30 -14
  5. package/extensions/orchestrator/ai-comment-cleanup.test.ts +89 -0
  6. package/extensions/orchestrator/ai-comment-cleanup.ts +73 -0
  7. package/extensions/orchestrator/command-handlers.test.ts +47 -0
  8. package/extensions/orchestrator/command-handlers.ts +43 -27
  9. package/extensions/orchestrator/config.test.ts +40 -1
  10. package/extensions/orchestrator/config.ts +13 -0
  11. package/extensions/orchestrator/context.ts +16 -0
  12. package/extensions/orchestrator/custom-footer.test.ts +91 -0
  13. package/extensions/orchestrator/custom-footer.ts +20 -20
  14. package/extensions/orchestrator/event-handlers.test.ts +315 -1
  15. package/extensions/orchestrator/event-handlers.ts +397 -201
  16. package/extensions/orchestrator/integration.test.ts +305 -9
  17. package/extensions/orchestrator/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  18. package/extensions/orchestrator/orchestrator.ts +46 -58
  19. package/extensions/orchestrator/phases/brainstorm.ts +11 -7
  20. package/extensions/orchestrator/phases/machine.test.ts +36 -0
  21. package/extensions/orchestrator/phases/machine.ts +11 -1
  22. package/extensions/orchestrator/phases/review-task.test.ts +20 -0
  23. package/extensions/orchestrator/phases/review-task.ts +44 -16
  24. package/extensions/orchestrator/phases/review.test.ts +26 -0
  25. package/extensions/orchestrator/phases/review.ts +40 -3
  26. package/extensions/orchestrator/pp-menu.test.ts +207 -1
  27. package/extensions/orchestrator/pp-menu.ts +376 -378
  28. package/extensions/orchestrator/state.test.ts +9 -0
  29. package/extensions/orchestrator/state.ts +15 -0
  30. package/extensions/orchestrator/transition-controller.test.ts +100 -0
  31. package/extensions/orchestrator/transition-controller.ts +61 -3
  32. package/package.json +1 -1
  33. package/AGENTS.md +0 -28
@@ -5,6 +5,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
5
5
  import lockfile from "proper-lockfile";
6
6
  import {
7
7
  createTask,
8
+ formatModeIndicator,
8
9
  getActiveTask,
9
10
  getEffectiveMode,
10
11
  getFirstPhase,
@@ -410,4 +411,12 @@ describe("getEffectiveMode", () => {
410
411
 
411
412
  expect(getEffectiveMode(state)).toBeUndefined();
412
413
  });
414
+
415
+ it("formatModeIndicator: autonomous stays autonomous, quick shows nothing, else guided (#4)", () => {
416
+ expect(formatModeIndicator({ ...baseState(), mode: "autonomous" }, "implement")).toBe("autonomous");
417
+ expect(formatModeIndicator({ ...baseState(), effectiveMode: "autonomous" }, "implement")).toBe("autonomous");
418
+ expect(formatModeIndicator({ ...baseState(), mode: "guided" }, "implement")).toBe("guided");
419
+ expect(formatModeIndicator({ ...baseState() }, "implement")).toBe("guided");
420
+ expect(formatModeIndicator({ ...baseState(), mode: "autonomous" }, "quick")).toBeUndefined();
421
+ });
413
422
  });
@@ -58,6 +58,14 @@ export interface TaskState {
58
58
  autonomousConfig?: AutonomousConfig;
59
59
  plannerFailureAutoRetried?: boolean;
60
60
  reviewerFailureAutoRetried?: boolean;
61
+ // Set once afterImplement has run for the current implement phase (either at
62
+ // the guided/autonomous transition or the autonomous terminal handoff), so the
63
+ // hooks never run twice for the same completion.
64
+ afterImplementRan?: boolean;
65
+ // Per-repo interleaved Plannotator review cursor (#3a). repoPaths is the ordered
66
+ // set of repos to review; index is the next repo. Persisted so the loop resumes
67
+ // on the next /pp after the agent fixes one repo's feedback. Undefined = no loop.
68
+ plannotatorCursor?: { repoPaths: string[]; index: number };
61
69
  }
62
70
 
63
71
  export interface TaskInfo {
@@ -93,6 +101,13 @@ export function getEffectivePhaseMode(state: TaskState): TaskMode {
93
101
  return getEffectiveMode(state) ?? "guided";
94
102
  }
95
103
 
104
+ // Display-only: reflects the task's chosen mode regardless of per-phase interactivity.
105
+ // Returns undefined for quick tasks so the footer omits the mode segment entirely.
106
+ export function formatModeIndicator(state: TaskState, type: TaskType): TaskMode | undefined {
107
+ if (type === "quick") return undefined;
108
+ return getEffectiveMode(state) === "autonomous" ? "autonomous" : "guided";
109
+ }
110
+
96
111
  export function createTask(cwd: string, type: TaskType, description: string, mode?: TaskMode): string {
97
112
  const log = getLogger();
98
113
  const id = crypto.randomUUID().slice(0, 12);
@@ -205,3 +205,103 @@ describe("TransitionController phase transition flow", () => {
205
205
  expect(onResume).toHaveBeenCalledOnce();
206
206
  });
207
207
  });
208
+
209
+ describe("TransitionController done supersession (task-boundary discard cannot be swallowed)", () => {
210
+ it("a done request supersedes a PENDING transition in place (no swallowed discard)", async () => {
211
+ const { host, session, calls, completeCompaction } = makeHost({ idle: false });
212
+ const c = new TransitionController(host, session);
213
+ // A phase transition is pending (not yet compacting: waiting for agent_end).
214
+ const staleResume = vi.fn();
215
+ const pPhase = c.requestTransition({ kind: "phase", summary: "phase", onResume: staleResume, instruction: "Begin working." });
216
+ expect(c.getState()).toBe("pending");
217
+ // A new-task done arrives before agent_end. It must replace the pending req.
218
+ const pDone = c.requestTransition({ kind: "done", discard: true, summary: "DISCARD" });
219
+ expect(c.getState()).toBe("pending");
220
+ expect(c.currentSummary()).toBe("DISCARD");
221
+ expect(c.isDiscardTransition()).toBe(true);
222
+
223
+ c.onAgentEnd();
224
+ expect(calls.compactStarted).toBe(1);
225
+ completeCompaction();
226
+ await Promise.all([pPhase, pDone]);
227
+ // The superseded phase resume/instruction must NOT have run.
228
+ expect(staleResume).not.toHaveBeenCalled();
229
+ expect(calls.userMessages).toHaveLength(0);
230
+ expect(c.getState()).toBe("running");
231
+ });
232
+
233
+ it("queues a done request behind a COMPACTING transition and runs its discard after", async () => {
234
+ const { host, session, calls, completeCompaction } = makeHost({ idle: false });
235
+ const c = new TransitionController(host, session);
236
+ const staleResume = vi.fn();
237
+ const pPhase = c.requestTransition({ kind: "phase", summary: "phase", onResume: staleResume, instruction: "Begin working." });
238
+ c.onAgentEnd();
239
+ expect(c.getState()).toBe("compacting");
240
+
241
+ // done arrives mid-compaction — must be queued, not dropped, not double-resumed.
242
+ let doneResolved = false;
243
+ const pDone = c.requestTransition({ kind: "done", discard: true, summary: "DISCARD" }).then(() => { doneResolved = true; });
244
+ expect(calls.compactStarted).toBe(1);
245
+
246
+ // First (phase) compaction settles. The superseded phase instruction must be
247
+ // suppressed because a done is queued behind it; the queued done then runs.
248
+ completeCompaction();
249
+ await pPhase;
250
+ expect(staleResume).toHaveBeenCalledOnce(); // phase onResume still ran (it was the active req)
251
+ // The queued done must not have resolved yet — its own compaction must run.
252
+ expect(doneResolved).toBe(false);
253
+ expect(calls.compactStarted).toBe(2);
254
+ expect(c.currentSummary()).toBe("DISCARD");
255
+ expect(c.isDiscardTransition()).toBe(true);
256
+
257
+ completeCompaction();
258
+ await pDone;
259
+ expect(doneResolved).toBe(true);
260
+ expect(c.getState()).toBe("running");
261
+ // The superseded phase instruction ("Begin working.") must NOT have been sent.
262
+ expect(calls.userMessages).toHaveLength(0);
263
+ });
264
+
265
+ it("queues a done request behind a RESUMING transition", async () => {
266
+ const { host, session, calls, completeCompaction } = makeHost({ idle: false });
267
+ const c = new TransitionController(host, session);
268
+ // onResume that lets us inject the done request while state === "resuming".
269
+ let injected: Promise<void> | null = null;
270
+ const pPhase = c.requestTransition({
271
+ kind: "phase",
272
+ summary: "phase",
273
+ onResume: () => {
274
+ expect(c.getState()).toBe("resuming");
275
+ injected = c.requestTransition({ kind: "done", discard: true, summary: "DISCARD" });
276
+ },
277
+ instruction: "Begin working.",
278
+ });
279
+ c.onAgentEnd();
280
+ completeCompaction();
281
+ await pPhase;
282
+ // A done was queued during resuming — it must run its own compaction now.
283
+ expect(injected).not.toBeNull();
284
+ expect(calls.compactStarted).toBe(2);
285
+ completeCompaction();
286
+ await injected!;
287
+ expect(c.getState()).toBe("running");
288
+ expect(c.isDiscardTransition()).toBe(false);
289
+ });
290
+
291
+ it("last-wins when two done requests queue behind an in-flight transition", async () => {
292
+ const { host, session, calls, completeCompaction } = makeHost({ idle: false });
293
+ const c = new TransitionController(host, session);
294
+ const pPhase = c.requestTransition({ kind: "phase", summary: "phase", onResume: () => {}, instruction: "go" });
295
+ c.onAgentEnd();
296
+ const pDone1 = c.requestTransition({ kind: "done", discard: true, summary: "FIRST" });
297
+ const pDone2 = c.requestTransition({ kind: "done", discard: true, summary: "SECOND" });
298
+
299
+ completeCompaction(); // phase settles -> queued done runs
300
+ await pPhase;
301
+ expect(c.currentSummary()).toBe("SECOND");
302
+ completeCompaction(); // done settles -> both queued callers resolve
303
+ await Promise.all([pDone1, pDone2]);
304
+ expect(c.getState()).toBe("running");
305
+ expect(calls.compactStarted).toBe(2);
306
+ });
307
+ });
@@ -26,6 +26,13 @@ export interface TransitionRequest {
26
26
  kind: TransitionKind;
27
27
  // Compaction summary used by the session_before_compact handler.
28
28
  summary?: string;
29
+ // Task-boundary discard: the previous task is finished/replaced and its
30
+ // conversation must be genuinely dropped (not softly summarized) so it cannot
31
+ // leak into the next task. Drives session_before_compact to keep no verbatim
32
+ // transcript beyond the summary. Only set on finished/replaced-task "done"
33
+ // transitions (startTask/finishTask/task-complete) — NOT pause/stop (those
34
+ // resume later and must preserve context).
35
+ discard?: boolean;
29
36
  // Runs after compaction completes (or is skipped), before the resume message.
30
37
  // Used for phase transitions to switch model + inject context/artifacts and to
31
38
  // spawn planners at the right moment. Throwing here is logged, not fatal.
@@ -80,6 +87,11 @@ export class TransitionController {
80
87
  private active: TransitionRequest | null = null;
81
88
  // Resolvers for callers that await the transition (done/stop/new-task paths).
82
89
  private waiters: Array<() => void> = [];
90
+ // A single "done" transition queued behind an in-flight (compacting/resuming)
91
+ // transition it supersedes. Run once the current transition settles so its
92
+ // discard compaction is guaranteed to execute. Last-wins (never a queue).
93
+ private pendingNext: TransitionRequest | null = null;
94
+ private pendingNextWaiters: Array<() => void> = [];
83
95
 
84
96
  constructor(
85
97
  private readonly host: TransitionHost,
@@ -141,6 +153,13 @@ export class TransitionController {
141
153
  return this.active?.summary ?? "";
142
154
  }
143
155
 
156
+ // True when the in-flight transition is a task-boundary discard (see
157
+ // TransitionRequest.discard). Lets session_before_compact drop the verbatim
158
+ // transcript window instead of keeping the default recent tokens.
159
+ isDiscardTransition(): boolean {
160
+ return this.active?.discard === true;
161
+ }
162
+
144
163
  // Outbound plain user-message. The ONLY path for main-session user messaging.
145
164
  // Plain user messages (pi.sendUserMessage) ALWAYS start a turn — even with
146
165
  // deliverAs:"steer" when idle (SDK prompt()) — so this only serves the
@@ -168,8 +187,29 @@ export class TransitionController {
168
187
  requestTransition(req: TransitionRequest): Promise<void> {
169
188
  const log = getLogger();
170
189
  if (this.state !== "running") {
171
- // A transition is already in flight. Coalesce: ignore the new request but
172
- // still attach the waiter so the caller is released when this settles.
190
+ // A transition is already in flight. A task-boundary discard (kind:"done")
191
+ // must NOT be silently skipped by an in-flight/aborted transition that
192
+ // is the concrete cause of prior-task context leaking into the next task.
193
+ if (req.kind === "done") {
194
+ if (this.state === "pending") {
195
+ // Not compacting yet (waiting for agent_end). Replace the pending
196
+ // request in place: its onResume/instruction is dropped (superseded),
197
+ // its waiters ride along and resolve when the done transition settles.
198
+ log.debug({ s: "controller", superseded: this.active?.kind }, "done supersedes pending transition");
199
+ this.active = req;
200
+ const promise = new Promise<void>((resolve) => this.waiters.push(resolve));
201
+ if (this.host.isIdle()) this.beginCompaction();
202
+ return promise;
203
+ }
204
+ // compacting/resuming: a compaction is already in flight for the old
205
+ // request. Queue the done request to run once it settles so its discard
206
+ // compaction actually executes; suppress the superseded instruction.
207
+ // Last-wins if one is already queued (never an unbounded queue).
208
+ log.debug({ s: "controller", state: this.state }, "done queued behind in-flight transition");
209
+ this.pendingNext = req;
210
+ return new Promise<void>((resolve) => this.pendingNextWaiters.push(resolve));
211
+ }
212
+ // phase (or other) while not running: keep conservative coalescing.
173
213
  if (req.kind === "phase") {
174
214
  log.warn({ s: "controller", state: this.state }, "phase transition coalesced while not running — onResume/instruction dropped");
175
215
  } else {
@@ -247,7 +287,10 @@ export class TransitionController {
247
287
  } catch (err: any) {
248
288
  getLogger().error({ s: "controller", err: err?.message ?? String(err) }, "onResume failed");
249
289
  }
250
- if (req?.instruction) {
290
+ // Suppress the superseded transition's resume instruction when a done
291
+ // request is queued to supersede it (its followUp would resume a task that
292
+ // is about to be discarded).
293
+ if (req?.instruction && !this.pendingNext) {
251
294
  this.send(req.instruction, "instruction");
252
295
  }
253
296
  this.active = null;
@@ -255,5 +298,20 @@ export class TransitionController {
255
298
  const waiters = this.waiters;
256
299
  this.waiters = [];
257
300
  for (const w of waiters) w();
301
+ // A done transition queued while this one was in flight: run it now. We are
302
+ // post-compaction with no agent turn in flight, so begin its compaction
303
+ // directly rather than through the isIdle/agent_end gate. Its queued callers
304
+ // ride the new transition's waiters, so they resolve only AFTER the discard
305
+ // compaction settles.
306
+ if (this.pendingNext) {
307
+ const next = this.pendingNext;
308
+ const resolvers = this.pendingNextWaiters;
309
+ this.pendingNext = null;
310
+ this.pendingNextWaiters = [];
311
+ this.active = next;
312
+ this.state = "pending";
313
+ for (const r of resolvers) this.waiters.push(r);
314
+ this.beginCompaction();
315
+ }
258
316
  }
259
317
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilya-lesikov/pi-pi",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Pi-Pi Coding Agent: based on Pi Coding Agent, but twice as good",
5
5
  "keywords": [
6
6
  "pi-package",
package/AGENTS.md DELETED
@@ -1,28 +0,0 @@
1
- # Repository Guidelines
2
-
3
- All rules in this document are requirements — not suggestions. ALWAYS follow them.
4
-
5
- ## Highest-priority rule (MANDATORY)
6
-
7
- - NEVER add comments unless they document a non-obvious public API or explain genuinely non-obvious logic. NEVER add comments that restate what the code does, repeat the field/function name, describe obvious error handling, or act as section separators. When in doubt, don't comment.
8
- - ALWAYS verify, don't assume — check the actual state before making changes.
9
- - ALWAYS start with the simplest possible solution. If it works, stop. Add complexity only when justified by a concrete, current requirement — NEVER for hypothetical future needs.
10
- - NEVER leave TODOs, stubs, or partial implementations.
11
- - ALWAYS stay within the scope of what was asked. When asked to update a plan — only update the plan, don't change code. When asked to brainstorm/discuss — only discuss, don't write code. When asked to do X — do X and nothing else. NEVER make unsolicited changes.
12
-
13
- ## Code style
14
-
15
- ### Design (MANDATORY)
16
-
17
- - ALWAYS prefer stupid and simple over abstract and extendable.
18
- - ALWAYS prefer a bit of duplication over complex abstractions.
19
- - ALWAYS prefer clarity over brevity in names.
20
- - ALWAYS minimize interfaces, generics, embedding.
21
- - ALWAYS prefer fewer types. Prefer no types over few. Prefer data types over types with behavior.
22
- - ALWAYS prefer functions over methods. ALWAYS prefer public fields over getters/setters.
23
- - ALWAYS keep everything private/internal as much as possible.
24
- - ALWAYS validate early, validate a lot. ALWAYS keep APIs stupid and minimal.
25
- - NEVER prefer global state. ALWAYS prefer simplicity over micro-optimizations.
26
- - ALWAYS use libraries for complex things instead of reinventing the wheel.
27
- - NEVER add comments unless they document a non-obvious public API or explain genuinely non-obvious logic. NEVER add obvious/redundant comments, NEVER add comments restating what code does. When in doubt, don't comment.
28
-