@ilya-lesikov/pi-pi 0.11.0 → 0.12.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 (57) hide show
  1. package/3p/pi-subagents/src/agent-manager.ts +1 -0
  2. package/3p/pi-subagents/src/index.ts +1 -0
  3. package/3p/pi-subagents/src/types.ts +8 -0
  4. package/extensions/orchestrator/agents/advisor.ts +2 -1
  5. package/extensions/orchestrator/agents/code-reviewer.ts +1 -0
  6. package/extensions/orchestrator/agents/constraints.test.ts +43 -10
  7. package/extensions/orchestrator/agents/constraints.ts +26 -5
  8. package/extensions/orchestrator/agents/deep-debugger.ts +7 -6
  9. package/extensions/orchestrator/agents/explore.ts +1 -1
  10. package/extensions/orchestrator/agents/librarian.ts +1 -1
  11. package/extensions/orchestrator/agents/planner.ts +1 -1
  12. package/extensions/orchestrator/agents/prompts.test.ts +135 -1
  13. package/extensions/orchestrator/agents/reviewer.ts +2 -2
  14. package/extensions/orchestrator/agents/task.ts +6 -2
  15. package/extensions/orchestrator/agents/tool-routing.ts +22 -6
  16. package/extensions/orchestrator/assumptions.test.ts +77 -0
  17. package/extensions/orchestrator/assumptions.ts +85 -0
  18. package/extensions/orchestrator/cbm.ts +4 -1
  19. package/extensions/orchestrator/cbm.which.test.ts +92 -0
  20. package/extensions/orchestrator/command-handlers.ts +9 -1
  21. package/extensions/orchestrator/config.test.ts +1 -3
  22. package/extensions/orchestrator/config.ts +3 -4
  23. package/extensions/orchestrator/context.test.ts +2 -7
  24. package/extensions/orchestrator/context.ts +3 -3
  25. package/extensions/orchestrator/doctor.test.ts +35 -0
  26. package/extensions/orchestrator/doctor.ts +4 -2
  27. package/extensions/orchestrator/event-handlers.more.test.ts +185 -0
  28. package/extensions/orchestrator/event-handlers.test.ts +22 -2
  29. package/extensions/orchestrator/event-handlers.ts +156 -18
  30. package/extensions/orchestrator/flant-infra.test.ts +0 -3
  31. package/extensions/orchestrator/flant-infra.ts +0 -1
  32. package/extensions/orchestrator/integration.test.ts +355 -173
  33. package/extensions/orchestrator/orchestrator.ts +3 -9
  34. package/extensions/orchestrator/phases/brainstorm.test.ts +25 -7
  35. package/extensions/orchestrator/phases/brainstorm.ts +35 -71
  36. package/extensions/orchestrator/phases/implementation.test.ts +22 -0
  37. package/extensions/orchestrator/phases/implementation.ts +6 -0
  38. package/extensions/orchestrator/phases/machine.test.ts +0 -28
  39. package/extensions/orchestrator/phases/machine.ts +0 -38
  40. package/extensions/orchestrator/phases/planning.test.ts +27 -1
  41. package/extensions/orchestrator/phases/planning.ts +1 -1
  42. package/extensions/orchestrator/phases/review-task.test.ts +7 -0
  43. package/extensions/orchestrator/phases/review-task.ts +1 -1
  44. package/extensions/orchestrator/phases/review.test.ts +47 -10
  45. package/extensions/orchestrator/phases/review.ts +36 -25
  46. package/extensions/orchestrator/pp-menu.from.test.ts +65 -0
  47. package/extensions/orchestrator/pp-menu.leaves.test.ts +17 -0
  48. package/extensions/orchestrator/pp-menu.test.ts +106 -49
  49. package/extensions/orchestrator/pp-menu.ts +279 -133
  50. package/extensions/orchestrator/pp-state-tools.ts +1 -1
  51. package/extensions/orchestrator/rate-limit-fallback-flow.test.ts +2 -2
  52. package/extensions/orchestrator/rate-limit-fallback.ts +1 -1
  53. package/extensions/orchestrator/state.test.ts +17 -17
  54. package/extensions/orchestrator/state.ts +35 -12
  55. package/package.json +2 -2
  56. package/scripts/postinstall.mjs +93 -0
  57. package/scripts/postinstall.sh +0 -18
@@ -138,7 +138,7 @@ function validateStateContent(taskDir: string, absolute: string, content: string
138
138
  const res = validatePlan(content);
139
139
  return res.ok
140
140
  ? { ok: true }
141
- : { ok: false, errors: res.errors, hint: "Keep exactly: # Plan, ## Scope, ## Checklist (items need 'Done when:'), ## Blockers (optional)." };
141
+ : { ok: false, errors: res.errors, hint: "Keep exactly: # Plan, ## Scope, ## Checklist (items need 'Done when:'), ## Pattern constraints (optional), ## Blockers (optional)." };
142
142
  }
143
143
  return { ok: true };
144
144
  }
@@ -30,14 +30,14 @@ import { handleMainRateLimit, handleSubagentRateLimit } from "./rate-limit-fallb
30
30
 
31
31
  function makeOrchestrator() {
32
32
  return {
33
- active: { state: { phase: "debug" } },
33
+ active: { state: { phase: "implement" } },
34
34
  activeTaskToken: 1,
35
35
  subFallbackActive: false,
36
36
  subFallbackDialogPending: false,
37
37
  subFallbackPendingDecision: false,
38
38
  subFallbackModelId: null as string | null,
39
39
  subSwitchBackTimer: null as any,
40
- config: { agents: { orchestrators: { debug: { thinking: "high" }, implement: { thinking: "high" } } } },
40
+ config: { agents: { orchestrators: { implement: { thinking: "high" } } } },
41
41
  switchModel: vi.fn().mockResolvedValue(true),
42
42
  sendUserMessageWhenIdle: vi.fn(),
43
43
  cancelPendingRetry: vi.fn(),
@@ -160,7 +160,7 @@ async function activateFallback(
160
160
  function currentThinking(orchestrator: Orchestrator): string {
161
161
  const phase = orchestrator.active?.state.phase;
162
162
  const orchestrators = orchestrator.config?.agents?.orchestrators as Record<string, { thinking?: string }> | undefined;
163
- const key = phase === "debug" || phase === "brainstorm" || phase === "review" || phase === "quick" ? phase : "implement";
163
+ const key = phase === "brainstorm" || phase === "review" || phase === "quick" ? phase : "implement";
164
164
  return orchestrators?.[key]?.thinking ?? "high";
165
165
  }
166
166
 
@@ -52,15 +52,6 @@ describe("createTask", () => {
52
52
  expect(raw.endsWith("\n")).toBe(true);
53
53
  });
54
54
 
55
- it("uses debug as initial phase for debug", () => {
56
- const cwd = makeCwd();
57
- const taskDir = createTask(cwd, "debug", "Fix timeout issue");
58
- const state = loadTask(taskDir);
59
- expect(state.phase).toBe("debug");
60
- expect(state.step).toBe("llm_work");
61
- expect(state.reviewCycle).toBeNull();
62
- expect(state.reviewPass).toBe(0);
63
- });
64
55
 
65
56
  it("uses brainstorm as initial phase for brainstorm", () => {
66
57
  const cwd = makeCwd();
@@ -103,6 +94,16 @@ describe("loadTask", () => {
103
94
 
104
95
  expect(() => loadTask(taskDir)).toThrowError(/Failed to parse .*state\.json:/);
105
96
  });
97
+
98
+ it("throws for a legacy debug-phase task", () => {
99
+ const cwd = makeCwd();
100
+ const taskDir = createTask(cwd, "implement", "Legacy debug");
101
+ const state = loadTask(taskDir);
102
+ (state as any).phase = "debug";
103
+ saveTask(taskDir, state);
104
+
105
+ expect(() => loadTask(taskDir)).toThrowError(/Debug tasks are no longer supported/);
106
+ });
106
107
  });
107
108
 
108
109
  describe("saveTask", () => {
@@ -134,18 +135,18 @@ describe("listTasks", () => {
134
135
  it("lists only non-done tasks and respects type filter", () => {
135
136
  const cwd = makeCwd();
136
137
  const implementTask = createTask(cwd, "implement", "Implement feature");
137
- const debugTask = createTask(cwd, "debug", "Debug crash");
138
+ const reviewTask = createTask(cwd, "review", "Review crash");
138
139
  const brainstormTask = createTask(cwd, "brainstorm", "Idea storm");
139
140
 
140
- const doneState = loadTask(debugTask);
141
+ const doneState = loadTask(reviewTask);
141
142
  doneState.phase = "done";
142
- saveTask(debugTask, doneState);
143
+ saveTask(reviewTask, doneState);
143
144
 
144
145
  const all = listTasks(cwd);
145
146
  const allDirs = all.map((t) => t.dir);
146
147
  expect(allDirs).toContain(implementTask);
147
148
  expect(allDirs).toContain(brainstormTask);
148
- expect(allDirs).not.toContain(debugTask);
149
+ expect(allDirs).not.toContain(reviewTask);
149
150
 
150
151
  const brainstormOnly = listTasks(cwd, "brainstorm");
151
152
  expect(brainstormOnly).toHaveLength(1);
@@ -384,7 +385,7 @@ describe("getActiveTask", () => {
384
385
  it("returns null when multiple unlocked tasks exist (ambiguous)", () => {
385
386
  const cwd = makeCwd();
386
387
  createTask(cwd, "implement", "First abandoned");
387
- createTask(cwd, "debug", "Second abandoned");
388
+ createTask(cwd, "review", "Second abandoned");
388
389
  vi.spyOn(lockfile, "checkSync").mockReturnValue(false);
389
390
 
390
391
  expect(getActiveTask(cwd)).toBeNull();
@@ -393,7 +394,7 @@ describe("getActiveTask", () => {
393
394
  it("returns the single unlocked task when others are locked", () => {
394
395
  const cwd = makeCwd();
395
396
  const lockedTask = createTask(cwd, "implement", "Locked by other");
396
- const unlockedTask = createTask(cwd, "debug", "Abandoned");
397
+ const unlockedTask = createTask(cwd, "review", "Abandoned");
397
398
 
398
399
  vi.spyOn(lockfile, "checkSync").mockImplementation((path: string, _options?: { stale?: number }) => {
399
400
  if (path === join(lockedTask, "state.json")) return true;
@@ -434,7 +435,7 @@ describe("getActiveTaskStatus", () => {
434
435
  it("reports ambiguous with all tasks when multiple are unlocked", () => {
435
436
  const cwd = makeCwd();
436
437
  createTask(cwd, "implement", "First");
437
- createTask(cwd, "debug", "Second");
438
+ createTask(cwd, "review", "Second");
438
439
  vi.spyOn(lockfile, "checkSync").mockReturnValue(false);
439
440
 
440
441
  const status = getActiveTaskStatus(cwd);
@@ -447,7 +448,6 @@ describe("getFirstPhase", () => {
447
448
  it("returns expected first phase for each task type", () => {
448
449
  expect(getFirstPhase("implement")).toBe("brainstorm");
449
450
  expect(getFirstPhase("brainstorm")).toBe("brainstorm");
450
- expect(getFirstPhase("debug")).toBe("debug");
451
451
  expect(getFirstPhase("review")).toBe("review");
452
452
  expect(getFirstPhase("quick")).toBe("quick");
453
453
  });
@@ -28,14 +28,13 @@ export interface AutonomousConfig {
28
28
  phases: Record<string, AutonomousPhaseConfig>;
29
29
  }
30
30
 
31
- export type TaskType = "implement" | "debug" | "brainstorm" | "review" | "quick";
31
+ export type TaskType = "implement" | "brainstorm" | "review" | "quick";
32
32
 
33
33
  export type ImplementPhase = "brainstorm" | "plan" | "implement" | "done";
34
- export type DebugPhase = "debug" | "plan" | "implement" | "done";
35
34
  export type BrainstormPhase = "brainstorm" | "plan" | "implement" | "done";
36
35
  export type ReviewPhase = "review" | "plan" | "implement" | "done";
37
36
  export type QuickPhase = "quick";
38
- export type Phase = ImplementPhase | DebugPhase | BrainstormPhase | ReviewPhase | QuickPhase;
37
+ export type Phase = ImplementPhase | BrainstormPhase | ReviewPhase | QuickPhase;
39
38
 
40
39
  export interface TaskState {
41
40
  phase: Phase;
@@ -76,10 +75,24 @@ export interface TaskState {
76
75
  // the guided/autonomous transition or the autonomous terminal handoff), so the
77
76
  // hooks never run twice for the same completion.
78
77
  afterImplementRan?: boolean;
79
- // Per-repo interleaved Plannotator review cursor (#3a). repoPaths is the ordered
80
- // set of repos to review; index is the next repo. Persisted so the loop resumes
81
- // on the next /pp after the agent fixes one repo's feedback. Undefined = no loop.
82
- plannotatorCursor?: { repoPaths: string[]; index: number };
78
+ // State-freshness reconcile markers (see maybePromptReconcile). Absent on legacy
79
+ // tasks. reconcilePending forces a re-prompt after a pause/complete that could
80
+ // not reconcile in-line.
81
+ reconcilePromptedPhase?: string;
82
+ reconciledPhase?: string;
83
+ reconcilePending?: boolean;
84
+ // Per-repo Plannotator review cursor. repoPaths is the ordered set of repos to
85
+ // review; index is read only by the single-repo path (retained for
86
+ // backward-compatible deserialization of older cursors). For multi-repo reviews
87
+ // the user drives repo selection via a status picker; `status` records each
88
+ // repo's outcome (absent = unreviewed) and `feedback` holds pending
89
+ // changes-requested feedback not yet applied. Undefined = no active loop.
90
+ plannotatorCursor?: {
91
+ repoPaths: string[];
92
+ index: number;
93
+ status?: Record<string, "approved" | "changes-requested" | "fixes-applied">;
94
+ feedback?: Record<string, string>;
95
+ };
83
96
  // The phase the task was in when it was marked done. Recorded so the Resume
84
97
  // "reopen a done task" flow (#2) can restore the actual last working phase
85
98
  // (done itself carries no phase history). Absent on legacy done tasks.
@@ -102,7 +115,6 @@ function taskStatePath(taskDir: string): string {
102
115
 
103
116
  export function getFirstPhase(type: TaskType): Exclude<Phase, "done"> {
104
117
  if (type === "implement" || type === "brainstorm") return "brainstorm";
105
- if (type === "debug") return "debug";
106
118
  if (type === "review") return "review";
107
119
  return "quick";
108
120
  }
@@ -115,7 +127,7 @@ export function getEffectiveMode(state: TaskState): TaskMode | undefined {
115
127
  // drives them, so they never inherit the task's autonomous mode even when one is set.
116
128
  // plan/implement are autonomous-capable regardless of whether they are the task's initial phase.
117
129
  export function getEffectivePhaseMode(state: TaskState): TaskMode {
118
- if (state.phase === "brainstorm" || state.phase === "debug" || state.phase === "review") return "guided";
130
+ if (state.phase === "brainstorm" || state.phase === "review") return "guided";
119
131
  return getEffectiveMode(state) ?? "guided";
120
132
  }
121
133
 
@@ -162,8 +174,19 @@ export function createTask(cwd: string, type: TaskType, description: string, mod
162
174
  export function loadTask(taskDir: string): TaskState {
163
175
  const sp = taskStatePath(taskDir);
164
176
  const raw = readFileSync(sp, "utf-8");
177
+ let parsed: TaskState;
178
+ try {
179
+ parsed = JSON.parse(raw) as TaskState;
180
+ } catch (err: any) {
181
+ throw new Error(`Failed to parse ${sp}: ${err.message}`);
182
+ }
183
+ if ((parsed as any).phase === "debug") {
184
+ throw new Error(
185
+ "Debug tasks are no longer supported; use /pp → Implement → From to reuse their artifacts.",
186
+ );
187
+ }
165
188
  try {
166
- const state = JSON.parse(raw) as TaskState;
189
+ const state = parsed;
167
190
  if ((state as any).phase === "planning") {
168
191
  state.phase = "plan";
169
192
  }
@@ -203,7 +226,7 @@ export function listTasks(cwd: string, typeOrOptions?: TaskType | ListTasksOptio
203
226
  const base = stateDir(cwd);
204
227
  if (!existsSync(base)) return [];
205
228
 
206
- const types: TaskType[] = options.type ? [options.type] : ["implement", "debug", "brainstorm", "review", "quick"];
229
+ const types: TaskType[] = options.type ? [options.type] : ["implement", "brainstorm", "review", "quick"];
207
230
  const results: TaskInfo[] = [];
208
231
 
209
232
  for (const t of types) {
@@ -343,7 +366,7 @@ function firstMarkdownContent(path: string): string | null {
343
366
  export function taskFullName(taskDir: string, state: TaskState): string {
344
367
  let desc = state.description ?? "";
345
368
 
346
- if (["implement", "debug", "brainstorm", "review", "quick"].includes(desc)) {
369
+ if (["implement", "brainstorm", "review", "quick"].includes(desc)) {
347
370
  const fallback =
348
371
  firstMarkdownContent(join(taskDir, "USER_REQUEST.md")) ??
349
372
  firstMarkdownContent(join(taskDir, "RESEARCH.md")) ??
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilya-lesikov/pi-pi",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Pi-Pi Coding Agent: based on Pi Coding Agent, but twice as good",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -33,7 +33,7 @@
33
33
  "AGENTS.md"
34
34
  ],
35
35
  "scripts": {
36
- "postinstall": "bash scripts/postinstall.sh",
36
+ "postinstall": "node scripts/postinstall.mjs",
37
37
  "test": "vitest run --exclude '3p/**'",
38
38
  "test:watch": "vitest --exclude '3p/**'",
39
39
  "test:3p": "bash scripts/test-3p.sh",
@@ -0,0 +1,93 @@
1
+ // Vendor-source decision: on non-Windows we invoke the existing bash `vendor.sh` UNCHANGED so
2
+ // Linux/CI stay byte-for-byte identical; on Windows we SKIP the bash vendor step (a documented
3
+ // Windows limitation) and rely on the npm-pack HTML fallback below. vendor.sh is still the single
4
+ // source used by `npm run build`/CI — this script does not fork its logic.
5
+ import { execFileSync, spawnSync } from "node:child_process";
6
+ import { existsSync, mkdtempSync, rmSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { gunzipSync } from "node:zlib";
8
+ import { tmpdir } from "node:os";
9
+ import { dirname, join, resolve } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+
12
+ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
13
+ const REPO_ROOT = resolve(SCRIPT_DIR, "..");
14
+ const PI_EXT_DIR = join(REPO_ROOT, "3p", "pi-plannotator", "apps", "pi-extension");
15
+ const isWindows = process.platform === "win32";
16
+
17
+ function fail(msg) {
18
+ console.error(` ✗ ${msg}`);
19
+ process.exit(1);
20
+ }
21
+
22
+ function runVendor() {
23
+ if (isWindows) {
24
+ console.log(" ⚠ skipping plannotator vendor step on Windows (bash vendor.sh unavailable)");
25
+ return;
26
+ }
27
+ const res = spawnSync("bash", [join(PI_EXT_DIR, "vendor.sh")], { cwd: REPO_ROOT, stdio: "inherit" });
28
+ if (res.status !== 0) fail(`vendor.sh failed with exit code ${res.status ?? "unknown"}`);
29
+ }
30
+
31
+ function htmlPresent() {
32
+ return existsSync(join(PI_EXT_DIR, "plannotator.html")) && existsSync(join(PI_EXT_DIR, "review-editor.html"));
33
+ }
34
+
35
+ // Extract a specific file from an npm tarball (gzipped POSIX/ustar tar) using only
36
+ // Node builtins — no external `tar` executable, which is not guaranteed on native
37
+ // Windows. Returns the file's bytes, or null when the entry is absent.
38
+ function extractFromTar(tarBytes, wantPath) {
39
+ let off = 0;
40
+ while (off + 512 <= tarBytes.length) {
41
+ const header = tarBytes.subarray(off, off + 512);
42
+ const name = header.subarray(0, 100).toString("utf-8").replace(/\0.*$/, "");
43
+ if (name === "") break;
44
+ const size = parseInt(header.subarray(124, 136).toString("utf-8").replace(/\0.*$/, "").trim() || "0", 8);
45
+ const dataStart = off + 512;
46
+ if (name === wantPath) return tarBytes.subarray(dataStart, dataStart + size);
47
+ off = dataStart + Math.ceil(size / 512) * 512;
48
+ }
49
+ return null;
50
+ }
51
+
52
+ // Run `npm pack` without depending on PATH/PATHEXT resolution of `npm`, which on
53
+ // Windows is `npm.cmd` and cannot be spawned by execFileSync without a shell
54
+ // (Node rejects .cmd without shell:true). In a lifecycle script npm exports
55
+ // npm_execpath (the CLI's JS entry); run it with the current node. Fall back to a
56
+ // shell-resolved `npm`/`npm.cmd` only if that env var is somehow absent.
57
+ function runNpmPack(destDir) {
58
+ const npmCli = process.env.npm_execpath;
59
+ if (npmCli) {
60
+ execFileSync(process.execPath, [npmCli, "pack", "@plannotator/pi-extension", "--pack-destination", destDir, "--silent"], {
61
+ stdio: ["ignore", "ignore", "ignore"],
62
+ });
63
+ return;
64
+ }
65
+ const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
66
+ execFileSync(npmCmd, ["pack", "@plannotator/pi-extension", "--pack-destination", destDir, "--silent"], {
67
+ stdio: ["ignore", "ignore", "ignore"],
68
+ shell: true,
69
+ });
70
+ }
71
+
72
+ function extractHtmlFallback() {
73
+ const tmp = mkdtempSync(join(tmpdir(), "pi-pi-plannotator-"));
74
+ try {
75
+ runNpmPack(tmp);
76
+ const tgz = readdirSync(tmp).find((name) => name.endsWith(".tgz"));
77
+ if (!tgz) fail("npm pack produced no tarball for @plannotator/pi-extension");
78
+ const tarBytes = gunzipSync(readFileSync(join(tmp, tgz)));
79
+ for (const file of ["plannotator.html", "review-editor.html"]) {
80
+ const bytes = extractFromTar(tarBytes, `package/${file}`);
81
+ if (!bytes) fail(`@plannotator/pi-extension tarball is missing ${file}`);
82
+ writeFileSync(join(PI_EXT_DIR, file), bytes);
83
+ }
84
+ } finally {
85
+ rmSync(tmp, { recursive: true, force: true });
86
+ }
87
+ }
88
+
89
+ runVendor();
90
+
91
+ if (htmlPresent()) process.exit(0);
92
+
93
+ extractHtmlFallback();
@@ -1,18 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- cd "$(dirname "$0")/.."
4
-
5
- bash 3p/pi-plannotator/apps/pi-extension/vendor.sh
6
-
7
- PI_EXT_DIR="3p/pi-plannotator/apps/pi-extension"
8
- if [ -f "$PI_EXT_DIR/plannotator.html" ] && [ -f "$PI_EXT_DIR/review-editor.html" ]; then
9
- exit 0
10
- fi
11
-
12
- TMPDIR=$(mktemp -d)
13
- trap 'rm -rf "$TMPDIR"' EXIT
14
-
15
- npm pack @plannotator/pi-extension --pack-destination "$TMPDIR" --silent 2>/dev/null
16
- tar -xzf "$TMPDIR"/*.tgz -C "$TMPDIR"
17
- cp "$TMPDIR/package/plannotator.html" "$PI_EXT_DIR/plannotator.html"
18
- cp "$TMPDIR/package/review-editor.html" "$PI_EXT_DIR/review-editor.html"