pi-plans 0.2.0 → 0.3.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 +74 -21
- package/index.ts +115 -9
- package/package.json +7 -1
- package/references/pi-planning-workflow.md +18 -3
- package/references/state-and-config.md +34 -2
- package/scripts/validate.ts +4 -0
- package/src/code-graph/commands.ts +437 -0
- package/src/code-graph/discovery.ts +118 -0
- package/src/code-graph/git.ts +108 -0
- package/src/code-graph/identity.ts +59 -0
- package/src/code-graph/indexer.ts +281 -0
- package/src/code-graph/materialize.ts +166 -0
- package/src/code-graph/mode.ts +28 -0
- package/src/code-graph/mutations.ts +160 -0
- package/src/code-graph/parser.ts +51 -0
- package/src/code-graph/parsers/javascript.ts +35 -0
- package/src/code-graph/parsers/python.ts +160 -0
- package/src/code-graph/parsers/tree-sitter.ts +316 -0
- package/src/code-graph/paths.ts +85 -0
- package/src/code-graph/prompts.ts +18 -0
- package/src/code-graph/resolver.ts +69 -0
- package/src/code-graph/runtime.ts +158 -0
- package/src/code-graph/schema.ts +135 -0
- package/src/code-graph/screening.ts +82 -0
- package/src/code-graph/store.ts +278 -0
- package/src/code-graph/summary.ts +435 -0
- package/src/code-graph/types.ts +163 -0
- package/src/compaction.ts +1125 -371
- package/src/config-command.ts +326 -0
- package/src/exec.ts +356 -686
- package/src/refine-prompts.ts +50 -0
- package/src/refine-ui-helpers.ts +71 -18
- package/src/refine-ui-state.ts +87 -21
- package/src/refine-ui.ts +210 -102
- package/src/state.ts +19 -6
- package/src/subagent.ts +163 -61
- package/tests/ask-choice.test.ts +263 -0
- package/tests/autocomplete.test.ts +6 -1
- package/tests/code-graph-apply.test.ts +185 -0
- package/tests/code-graph-commands.test.ts +211 -0
- package/tests/code-graph-db.test.ts +166 -0
- package/tests/code-graph-discovery.test.ts +38 -0
- package/tests/code-graph-git.test.ts +94 -0
- package/tests/code-graph-index.test.ts +175 -0
- package/tests/code-graph-loop.e2e.test.ts +159 -0
- package/tests/code-graph-mutations.test.ts +117 -0
- package/tests/code-graph-parser.test.ts +85 -0
- package/tests/code-graph-rollback.test.ts +100 -0
- package/tests/code-graph-summary-batching.test.ts +518 -0
- package/tests/code-graph-summary.test.ts +148 -0
- package/tests/compaction.test.ts +371 -57
- package/tests/config-command.test.ts +255 -0
- package/tests/exec.test.ts +665 -241
- package/tests/fixtures/code-graph/sample.js +36 -0
- package/tests/fixtures/code-graph/sample.py +20 -0
- package/tests/fixtures/code-graph/sample.ts +15 -0
- package/tests/graph-aware-file-tools.test.ts +411 -0
- package/tests/refine-prompts.test.ts +67 -2
- package/tests/refine-ui.test.ts +337 -72
- package/tests/subagent.test.ts +26 -20
- package/tools/ask-choice.ts +158 -11
- package/tools/code-graph.ts +254 -0
- package/tools/graph-aware-file-tools.ts +392 -0
- package/tools/plans.ts +84 -1
- package/tools/refine.ts +61 -15
package/src/refine-prompts.ts
CHANGED
|
@@ -30,6 +30,23 @@ Authority boundary: read-only analysis only. Do not edit, write, delete, commit,
|
|
|
30
30
|
Evidence: inspect the repository with read, grep, find, and ls before judging the plan.${lensLine}${focusLine}${contextLine}`;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** Shared header for the post-execution implementation review: the
|
|
34
|
+
* accepted plan is the contract, the IMPLEMENTATION in the worktree is
|
|
35
|
+
* under review. Findings must anchor to the plan's goals/acceptance
|
|
36
|
+
* criteria and explicitly assess delivery maturity. */
|
|
37
|
+
function buildImplementationSharedHeader(role: "reviewer" | "criticizer", opts: RefinePromptInput): string {
|
|
38
|
+
const lensLine = role === "reviewer" && opts.lens ? `\nReview lens: ${opts.lens}.` : "";
|
|
39
|
+
const focusLine = opts.focus ? `\n\nSpecific concerns from the main agent: ${opts.focus}` : "";
|
|
40
|
+
const contextLine = opts.context ? `\n\nContext: ${opts.context}` : "";
|
|
41
|
+
return `Goal: ${role === "reviewer" ? "review the implemented result in the worktree against the plan" : "stress-test the implemented result's assumptions"}.
|
|
42
|
+
|
|
43
|
+
Target: ${opts.planPath} (the accepted plan; the IMPLEMENTATION in the worktree is under review)
|
|
44
|
+
|
|
45
|
+
Authority boundary: read-only analysis only. Do not edit, write, delete, commit, push, or spawn subagents.
|
|
46
|
+
|
|
47
|
+
Evidence: inspect the repository with read, grep, find, and ls before judging the implementation. Judge the implementation against the plan's goals, verifier checklist, and acceptance criteria. Explicitly assess delivery maturity: did the executor ship a minimal MVP only, or refine for long-term growth (no stopgaps, long-term architectural decisions, missing tests, technical debt, production readiness)? Calibrate severity accordingly. Out-of-scope improvement ideas are low severity by default and must not be forced into findings.${lensLine}${focusLine}${contextLine}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
33
50
|
export function reviewerLanes(count: number): ReviewerLane[] {
|
|
34
51
|
if (count === 3) return [...REVIEWER_LENSES];
|
|
35
52
|
if (count === 2) return [...REVIEWER_LENSES.slice(0, 2)];
|
|
@@ -68,3 +85,36 @@ Plan file: ${opts.planPath}
|
|
|
68
85
|
${opts.planText}
|
|
69
86
|
---8<--- END PLAN CONTENT ---8<---`;
|
|
70
87
|
}
|
|
88
|
+
|
|
89
|
+
export function buildImplementationReviewerTask(opts: RefinePromptInput): string {
|
|
90
|
+
return `${buildImplementationSharedHeader("reviewer", opts)}
|
|
91
|
+
|
|
92
|
+
Success criteria: return evidence-backed findings or explicitly say the implementation holds up.
|
|
93
|
+
|
|
94
|
+
Output: Markdown, highest severity first. For each finding use this shape:
|
|
95
|
+
- \`F-###\` — severity: high | medium | low; affected plan IDs (e.g. R-001, I-003) or files; evidence: repo path/command that proves it; impact; recommended fix; suggested disposition (accept | reject | needs-discussion).
|
|
96
|
+
|
|
97
|
+
Surface at most five high-priority findings; list lower-severity findings after them. If the implementation holds up, say so explicitly and list what you checked.
|
|
98
|
+
|
|
99
|
+
Plan file: ${opts.planPath}
|
|
100
|
+
|
|
101
|
+
---8<--- PLAN CONTENT ---8<---
|
|
102
|
+
${opts.planText}
|
|
103
|
+
---8<--- END PLAN CONTENT ---8<---`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function buildImplementationCriticizerTask(opts: RefinePromptInput): string {
|
|
107
|
+
return `${buildImplementationSharedHeader("criticizer", opts)}
|
|
108
|
+
|
|
109
|
+
Success criteria: return concrete, answerable questions only; never rewrite the plan or the implementation.
|
|
110
|
+
|
|
111
|
+
Output: Markdown in exactly this shape:
|
|
112
|
+
1. A summary of your core criticism in at most three sentences, highlighting the single most important point.
|
|
113
|
+
2. Then at most five adaptive questions, numbered, each with one line of why it matters. Questions must be answerable by a user with repo access — never rhetorical. Stop earlier if the implementation genuinely holds.
|
|
114
|
+
|
|
115
|
+
Plan file: ${opts.planPath}
|
|
116
|
+
|
|
117
|
+
---8<--- PLAN CONTENT ---8<---
|
|
118
|
+
${opts.planText}
|
|
119
|
+
---8<--- END PLAN CONTENT ---8<---`;
|
|
120
|
+
}
|
package/src/refine-ui-helpers.ts
CHANGED
|
@@ -9,8 +9,32 @@
|
|
|
9
9
|
|
|
10
10
|
const ELLIPSIS = "…";
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* ECMA-48 CSI sequence: ESC [ parameter bytes (0x30-0x3F), intermediate bytes
|
|
14
|
+
* (0x20-0x2F), final byte (0x40-0x7E). Matched atomically so styling payloads
|
|
15
|
+
* never leak into width math and are never split mid-sequence.
|
|
16
|
+
*/
|
|
17
|
+
const CSI_PATTERN = /\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g;
|
|
18
|
+
|
|
19
|
+
interface AnsiPart {
|
|
20
|
+
kind: "csi" | "text";
|
|
21
|
+
value: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function splitAnsi(text: string): AnsiPart[] {
|
|
25
|
+
const parts: AnsiPart[] = [];
|
|
26
|
+
let last = 0;
|
|
27
|
+
for (const match of text.matchAll(CSI_PATTERN)) {
|
|
28
|
+
const start = match.index ?? 0;
|
|
29
|
+
if (start > last) parts.push({ kind: "text", value: text.slice(last, start) });
|
|
30
|
+
parts.push({ kind: "csi", value: match[0] });
|
|
31
|
+
last = start + match[0].length;
|
|
32
|
+
}
|
|
33
|
+
if (last < text.length) parts.push({ kind: "text", value: text.slice(last) });
|
|
34
|
+
return parts;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function graphemeWidth(text: string): number {
|
|
14
38
|
let width = 0;
|
|
15
39
|
for (const segment of new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(text)) {
|
|
16
40
|
const cp = segment.segment.codePointAt(0) ?? 0;
|
|
@@ -26,6 +50,16 @@ export function visibleWidth(text: string): number {
|
|
|
26
50
|
return width;
|
|
27
51
|
}
|
|
28
52
|
|
|
53
|
+
export function visibleWidth(text: string): number {
|
|
54
|
+
if (!text) return 0;
|
|
55
|
+
let width = 0;
|
|
56
|
+
for (const part of splitAnsi(text)) {
|
|
57
|
+
if (part.kind === "csi") continue; // escape sequences render at zero width
|
|
58
|
+
width += graphemeWidth(part.value);
|
|
59
|
+
}
|
|
60
|
+
return width;
|
|
61
|
+
}
|
|
62
|
+
|
|
29
63
|
export function truncateToWidth(text: string, maxWidth: number, ellipsis = ELLIPSIS): string {
|
|
30
64
|
const measured = visibleWidth(text);
|
|
31
65
|
if (measured <= maxWidth) return text;
|
|
@@ -33,12 +67,25 @@ export function truncateToWidth(text: string, maxWidth: number, ellipsis = ELLIP
|
|
|
33
67
|
const target = Math.max(0, maxWidth - ellipsisWidth);
|
|
34
68
|
let result = "";
|
|
35
69
|
let used = 0;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
70
|
+
let exhausted = false;
|
|
71
|
+
for (const part of splitAnsi(text)) {
|
|
72
|
+
if (part.kind === "csi") {
|
|
73
|
+
if (!exhausted) result += part.value; // keep styling runs intact; never split them
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
for (const segment of new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(part.value)) {
|
|
77
|
+
const w = visibleWidth(segment.segment);
|
|
78
|
+
if (used + w > target) {
|
|
79
|
+
exhausted = true;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
result += segment.segment;
|
|
83
|
+
used += w;
|
|
84
|
+
}
|
|
41
85
|
}
|
|
86
|
+
// A kept open SGR (its reset lived past the cut) must not bleed into the
|
|
87
|
+
// ellipsis or anything rendered after this string.
|
|
88
|
+
if (exhausted && result.includes("\x1b[")) result += "\x1b[0m";
|
|
42
89
|
return ellipsis ? `${result}${ellipsis}` : result;
|
|
43
90
|
}
|
|
44
91
|
|
|
@@ -53,19 +100,25 @@ export function wrapTextWithAnsi(text: string, maxWidth: number): string[] {
|
|
|
53
100
|
if (word === "") continue;
|
|
54
101
|
const wordWidth = visibleWidth(word);
|
|
55
102
|
if (wordWidth > maxWidth) {
|
|
56
|
-
// Hard split a single overlong word.
|
|
103
|
+
// Hard split a single overlong word (ANSI-aware: CSI runs are zero-width and atomic).
|
|
57
104
|
if (current) { lines.push(current); current = ""; currentWidth = 0; }
|
|
58
105
|
let buffer = "";
|
|
59
106
|
let bufferWidth = 0;
|
|
60
|
-
for (const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
107
|
+
for (const part of splitAnsi(word)) {
|
|
108
|
+
if (part.kind === "csi") {
|
|
109
|
+
buffer += part.value;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
for (const segment of new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(part.value)) {
|
|
113
|
+
const w = visibleWidth(segment.segment);
|
|
114
|
+
if (bufferWidth + w > maxWidth) {
|
|
115
|
+
lines.push(buffer);
|
|
116
|
+
buffer = segment.segment;
|
|
117
|
+
bufferWidth = w;
|
|
118
|
+
} else {
|
|
119
|
+
buffer += segment.segment;
|
|
120
|
+
bufferWidth += w;
|
|
121
|
+
}
|
|
69
122
|
}
|
|
70
123
|
}
|
|
71
124
|
if (buffer) { lines.push(buffer); buffer = ""; bufferWidth = 0; }
|
|
@@ -86,4 +139,4 @@ export function wrapTextWithAnsi(text: string, maxWidth: number): string[] {
|
|
|
86
139
|
|
|
87
140
|
export function matchesEscape(data: string): boolean {
|
|
88
141
|
return data === "\x1b" || data === "\x1b\x1b" || /^(\x1b\[\??\d*[A-Za-z])|(\x1bO[A-Za-z])$/.test(data);
|
|
89
|
-
}
|
|
142
|
+
}
|
package/src/refine-ui-state.ts
CHANGED
|
@@ -2,6 +2,17 @@ import type { SubagentProgressEvent, SubagentResult } from "./subagent.ts";
|
|
|
2
2
|
|
|
3
3
|
export type RefineOverlayRole = "reviewer" | "criticizer";
|
|
4
4
|
export type RefineLaneStatus = "queued" | "running" | "complete" | "failed" | "cancelled";
|
|
5
|
+
export type RefineTranscriptEntryType = "assistant-text" | "thinking" | "tool-call" | "tool-result" | "diagnostic";
|
|
6
|
+
|
|
7
|
+
export interface RefineTranscriptEntry {
|
|
8
|
+
id: string;
|
|
9
|
+
type: RefineTranscriptEntryType;
|
|
10
|
+
text: string;
|
|
11
|
+
streaming: boolean;
|
|
12
|
+
toolCallId?: string;
|
|
13
|
+
toolName?: string;
|
|
14
|
+
isError?: boolean;
|
|
15
|
+
}
|
|
5
16
|
|
|
6
17
|
export interface RefineLaneState {
|
|
7
18
|
id: string;
|
|
@@ -9,11 +20,11 @@ export interface RefineLaneState {
|
|
|
9
20
|
status: RefineLaneStatus;
|
|
10
21
|
phase: string;
|
|
11
22
|
detail: string;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
23
|
+
transcript: RefineTranscriptEntry[];
|
|
24
|
+
currentTurnIndex: number;
|
|
25
|
+
scrollOffset: number;
|
|
26
|
+
followTranscript: boolean;
|
|
27
|
+
viewportHeight: number;
|
|
17
28
|
}
|
|
18
29
|
|
|
19
30
|
export function statusLabel(status: RefineLaneStatus): string {
|
|
@@ -31,6 +42,56 @@ export function statusLabel(status: RefineLaneStatus): string {
|
|
|
31
42
|
}
|
|
32
43
|
}
|
|
33
44
|
|
|
45
|
+
function transcriptId(lane: RefineLaneState, event: Extract<SubagentProgressEvent, { type: "transcript" }>): string {
|
|
46
|
+
return `${lane.currentTurnIndex}:${event.key}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function phaseForEntry(event: Extract<SubagentProgressEvent, { type: "transcript" }>): string {
|
|
50
|
+
switch (event.entryType) {
|
|
51
|
+
case "assistant-text":
|
|
52
|
+
return "responding";
|
|
53
|
+
case "thinking":
|
|
54
|
+
return "thinking";
|
|
55
|
+
case "tool-call":
|
|
56
|
+
return event.toolName ? `tool: ${event.toolName}` : "tool call";
|
|
57
|
+
case "tool-result":
|
|
58
|
+
return event.toolName ? `result: ${event.toolName}` : "tool result";
|
|
59
|
+
case "diagnostic":
|
|
60
|
+
return "diagnostic";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function applyTranscriptEvent(lane: RefineLaneState, event: Extract<SubagentProgressEvent, { type: "transcript" }>): void {
|
|
65
|
+
const id = transcriptId(lane, event);
|
|
66
|
+
let entry = lane.transcript.find((candidate) => candidate.id === id);
|
|
67
|
+
if (!entry && event.entryType === "tool-call" && event.toolCallId) {
|
|
68
|
+
entry = lane.transcript.find((candidate) => candidate.type === "tool-call" && candidate.toolCallId === event.toolCallId);
|
|
69
|
+
}
|
|
70
|
+
if (!entry) {
|
|
71
|
+
entry = { id, type: event.entryType, text: "", streaming: event.streaming };
|
|
72
|
+
lane.transcript.push(entry);
|
|
73
|
+
}
|
|
74
|
+
if (event.update === "append") entry.text += event.text;
|
|
75
|
+
else entry.text = event.text;
|
|
76
|
+
entry.streaming = event.streaming;
|
|
77
|
+
if (event.toolCallId !== undefined) entry.toolCallId = event.toolCallId;
|
|
78
|
+
if (event.toolName !== undefined) entry.toolName = event.toolName;
|
|
79
|
+
if (event.isError !== undefined) entry.isError = event.isError;
|
|
80
|
+
lane.detail = entry.text;
|
|
81
|
+
lane.status = "running";
|
|
82
|
+
lane.phase = phaseForEntry(event);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function applyDiagnostic(lane: RefineLaneState, text: string): void {
|
|
86
|
+
if (!text) return;
|
|
87
|
+
const id = `${lane.currentTurnIndex}:diagnostic`;
|
|
88
|
+
const existing = lane.transcript.find((entry) => entry.id === id);
|
|
89
|
+
if (existing) existing.text += text;
|
|
90
|
+
else lane.transcript.push({ id, type: "diagnostic", text, streaming: true });
|
|
91
|
+
lane.detail = text;
|
|
92
|
+
lane.phase = "diagnostic";
|
|
93
|
+
}
|
|
94
|
+
|
|
34
95
|
export function applyRefineProgress(lane: RefineLaneState, event: SubagentProgressEvent): void {
|
|
35
96
|
if (lane.status === "complete" || lane.status === "failed" || lane.status === "cancelled") return;
|
|
36
97
|
|
|
@@ -39,40 +100,45 @@ export function applyRefineProgress(lane: RefineLaneState, event: SubagentProgre
|
|
|
39
100
|
lane.phase = event.phase === "started" ? "starting" : "exiting";
|
|
40
101
|
return;
|
|
41
102
|
case "turn":
|
|
42
|
-
|
|
43
|
-
|
|
103
|
+
if (event.phase === "start") {
|
|
104
|
+
lane.status = "running";
|
|
105
|
+
lane.phase = "thinking";
|
|
106
|
+
lane.currentTurnIndex = event.turnIndex ?? lane.currentTurnIndex + 1;
|
|
107
|
+
} else {
|
|
108
|
+
lane.phase = "waiting";
|
|
109
|
+
}
|
|
44
110
|
return;
|
|
45
|
-
case "
|
|
46
|
-
lane
|
|
47
|
-
lane.phase = event.role === "assistant" ? "responding" : event.role;
|
|
48
|
-
if (event.text) lane.detail = shorten(event.text, 180);
|
|
49
|
-
return;
|
|
50
|
-
case "tool":
|
|
51
|
-
lane.status = "running";
|
|
52
|
-
lane.phase = `tool: ${event.toolName}`;
|
|
53
|
-
if (event.detail) lane.detail = shorten(event.detail, 180);
|
|
111
|
+
case "transcript":
|
|
112
|
+
applyTranscriptEvent(lane, event);
|
|
54
113
|
return;
|
|
55
114
|
case "stderr":
|
|
56
|
-
lane.
|
|
57
|
-
if (event.text) lane.detail = shorten(event.text, 180);
|
|
115
|
+
applyDiagnostic(lane, event.text);
|
|
58
116
|
return;
|
|
59
117
|
}
|
|
60
118
|
}
|
|
61
119
|
|
|
62
120
|
export function applyRefineResult(lane: RefineLaneState, result: SubagentResult): void {
|
|
121
|
+
const finalText = result.output || result.errorMessage || result.stderr || "";
|
|
122
|
+
const finalEntry = [...lane.transcript].reverse().find((entry) => entry.type === "assistant-text");
|
|
123
|
+
if (finalText) {
|
|
124
|
+
if (finalEntry) {
|
|
125
|
+
finalEntry.text = finalText;
|
|
126
|
+
finalEntry.streaming = false;
|
|
127
|
+
} else {
|
|
128
|
+
lane.transcript.push({ id: `${lane.currentTurnIndex}:result`, type: "assistant-text", text: finalText, streaming: false });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
lane.detail = finalText;
|
|
63
132
|
if (result.ok) {
|
|
64
133
|
lane.status = "complete";
|
|
65
134
|
lane.phase = "complete";
|
|
66
|
-
lane.detail = shorten(result.output, 180);
|
|
67
135
|
return;
|
|
68
136
|
}
|
|
69
137
|
if (result.cancelled) {
|
|
70
138
|
lane.status = "cancelled";
|
|
71
139
|
lane.phase = "cancelled";
|
|
72
|
-
lane.detail = shorten(result.errorMessage ?? "Subagent was aborted", 180);
|
|
73
140
|
return;
|
|
74
141
|
}
|
|
75
142
|
lane.status = "failed";
|
|
76
143
|
lane.phase = result.timedOut ? "timed out" : "failed";
|
|
77
|
-
lane.detail = shorten((result.errorMessage ?? result.stderr) || "Subagent failed", 180);
|
|
78
144
|
}
|