pi-plans 0.3.0 → 0.3.2
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 +22 -11
- package/agents/ref-analyst.md +18 -0
- package/index.ts +12 -14
- package/package.json +11 -2
- package/references/pi-planning-workflow.md +26 -5
- package/references/state-and-config.md +21 -6
- package/scripts/validate.ts +1 -0
- package/skills/plan-with-refs/SKILL.md +3 -3
- package/src/code-graph/commands.ts +73 -27
- package/src/code-graph/prompts.ts +1 -1
- package/src/config-command.ts +35 -0
- package/src/exec.ts +237 -5
- package/src/guard.ts +14 -1
- package/src/refine-prompts.ts +59 -0
- package/src/refine-ui-state.ts +1 -1
- package/src/refine-ui.ts +1 -1
- package/src/state.ts +17 -1
- package/src/subagent.ts +1 -0
- package/src/termination-prompt.ts +22 -0
- package/tests/analyze-refs.test.ts +265 -0
- package/tests/ask-choice.test.ts +1 -0
- package/tests/code-graph-apply-action.test.ts +173 -0
- package/tests/config-command.test.ts +8 -0
- package/tests/exec-lifecycle.test.ts +137 -0
- package/tests/exec.test.ts +15 -2
- package/tests/goal-wait.test.ts +269 -0
- package/tests/guard.test.ts +27 -1
- package/tests/plans.test.ts +10 -0
- package/tests/refine-prompts.test.ts +35 -1
- package/tests/refine-ui.test.ts +34 -0
- package/tests/state.test.ts +32 -0
- package/tests/subagent.test.ts +22 -0
- package/tools/analyze-refs.ts +263 -0
- package/tools/ask-choice.ts +2 -1
- package/tools/code-graph.ts +25 -2
- package/tools/execute-plan.ts +18 -0
- package/tools/graph-aware-file-tools.ts +2 -2
- package/tools/plans.ts +14 -2
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `analyze_refs` tool — plan-with-refs per-reference analysis via read-only Pi
|
|
3
|
+
* subagents with isolated context. One lane per reference (cwd = the ref's own
|
|
4
|
+
* directory), reusing the reviewer role gates from `.git/pi_plans/config.json`
|
|
5
|
+
* and the concurrent refinement overlay (title "Refs"). Batches are capped at
|
|
6
|
+
* three concurrent lanes; larger ref sets run as sequential batches.
|
|
7
|
+
*
|
|
8
|
+
* Recording is best-effort: spawns land in `subagents.jsonl` (role
|
|
9
|
+
* `ref-analyst`) only when an active planning run exists. Analysis output is
|
|
10
|
+
* returned to the main agent, which owns REF_ANALYSIS.md and refs.jsonl.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
14
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { truncateHead } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
17
|
+
import { Type } from "typebox";
|
|
18
|
+
import * as fs from "node:fs";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
import { loadConfig, normalizeWorkdir, readActive, recordSubagent, resolveStateRootOrNull, StateError } from "../src/state.ts";
|
|
21
|
+
import { buildRefAnalystTask, type RefAnalystTaskInput } from "../src/refine-prompts.ts";
|
|
22
|
+
import { runPiSubagent, stripFrontmatter } from "../src/subagent.ts";
|
|
23
|
+
import { RefineOverlayController, refineOverlayContext } from "../src/refine-ui.ts";
|
|
24
|
+
|
|
25
|
+
const BATCH_SIZE = 3;
|
|
26
|
+
const READ_ONLY_TOOLS = ["read", "grep", "find", "ls"];
|
|
27
|
+
|
|
28
|
+
const AnalyzeRefsParams = Type.Object({
|
|
29
|
+
refs: Type.Array(
|
|
30
|
+
Type.Object({
|
|
31
|
+
id: Type.String({ description: "Stable ref id, e.g. ref-1; used in lane names and result sections" }),
|
|
32
|
+
localPath: Type.String({ description: "Local directory of the downloaded reference (absolute or relative to workdir)" }),
|
|
33
|
+
title: Type.Optional(Type.String({ description: "Reference title" })),
|
|
34
|
+
url: Type.Optional(Type.String({ description: "Source URL" })),
|
|
35
|
+
kind: Type.Optional(Type.String({ description: "Reference kind, e.g. project | article | paper | docs" })),
|
|
36
|
+
}),
|
|
37
|
+
{ description: "Downloaded references to analyze; each gets its own independent read-only subagent" },
|
|
38
|
+
),
|
|
39
|
+
context: Type.Optional(
|
|
40
|
+
Type.String({ description: "Target repo context for adoptability judgments: user goals, repo evidence, constraints" }),
|
|
41
|
+
),
|
|
42
|
+
workdir: Type.Optional(Type.String({ description: "Target workspace; default current working directory" })),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function gateError(problem: "state" | "mode" | "current-session" | "confirm"): StateError {
|
|
46
|
+
if (problem === "state") {
|
|
47
|
+
return new StateError("no pi-plans state found; run the plans tool (action: init) first");
|
|
48
|
+
}
|
|
49
|
+
if (problem === "mode") {
|
|
50
|
+
return new StateError(
|
|
51
|
+
"The reviewer role mode is missing or invalid in .git/pi_plans/config.json (analyze_refs reuses the reviewer gates). Ask the role-setting question with ask_choice first: 1. Delegated subagent (recommended; read-only pi subprocess with isolated context) 2. Current session 3. Other 4. Auto-complete — then persist with the plans tool (set-role, role=reviewer).",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (problem === "current-session") {
|
|
55
|
+
return new StateError(
|
|
56
|
+
"The reviewer role mode is current-session, but analyze_refs only spawns delegated read-only subagents (one per reference). Ask the user to switch the reviewer mode to delegated-subagent via ask_choice, persist with the plans tool (set-role, role=reviewer, mode=delegated-subagent), then retry analyze_refs.",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return new StateError(
|
|
60
|
+
"The reviewer model was never confirmed (confirmed_at is null); analyze_refs reuses the reviewer confirmation. Ask the model-confirmation question with ask_choice: 1. Inherit the main agent's model (recommended) 2. Choose a model (list options from the /model picker; persist the exact provider/model selector) 3. Other 4. Auto-complete — then persist with the plans tool (set-role, role=reviewer, confirmed: true, modelSelector: the selector or 'inherit').",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface AnalysisJob {
|
|
65
|
+
input: RefAnalystTaskInput;
|
|
66
|
+
name: string;
|
|
67
|
+
laneId: string;
|
|
68
|
+
dir: string;
|
|
69
|
+
missing: string | null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function registerAnalyzeRefsTool(pi: ExtensionAPI, baseDir: string): void {
|
|
73
|
+
const agentPrompt = stripFrontmatter(fs.readFileSync(path.join(baseDir, "agents", "ref-analyst.md"), "utf8"));
|
|
74
|
+
|
|
75
|
+
pi.registerTool({
|
|
76
|
+
name: "analyze_refs",
|
|
77
|
+
label: "Analyze Refs",
|
|
78
|
+
description:
|
|
79
|
+
"plan-with-refs: analyze downloaded references via independent read-only Pi subagents — one lane per reference (cwd = the ref directory), reusing the reviewer role gates and the concurrent overlay. Batches of at most 3 lanes run sequentially; results are structured per-reference sections for REF_ANALYSIS.md. Recording into subagents.jsonl is best-effort (active run only); refs.jsonl stays owned by the main agent via the plans record-ref action. Refuses until the reviewer mode/model gates pass in .git/pi_plans/config.json.",
|
|
80
|
+
promptSnippet: "Analyze plan-with-refs references with per-ref read-only subagents",
|
|
81
|
+
parameters: AnalyzeRefsParams,
|
|
82
|
+
|
|
83
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
84
|
+
const workdir = normalizeWorkdir(params.workdir ?? ctx.cwd);
|
|
85
|
+
|
|
86
|
+
// Read config read-only; state must already exist (same precondition as refine).
|
|
87
|
+
const root = resolveStateRootOrNull(workdir);
|
|
88
|
+
if (root === null || !fs.existsSync(path.join(root, "config.json"))) {
|
|
89
|
+
throw gateError("state");
|
|
90
|
+
}
|
|
91
|
+
const config = loadConfig(root);
|
|
92
|
+
const reviewer = config.reviewer;
|
|
93
|
+
if (!reviewer || (reviewer.mode !== "delegated-subagent" && reviewer.mode !== "current-session")) {
|
|
94
|
+
throw gateError("mode");
|
|
95
|
+
}
|
|
96
|
+
if (reviewer.mode === "current-session") {
|
|
97
|
+
throw gateError("current-session");
|
|
98
|
+
}
|
|
99
|
+
if (reviewer.confirmed_at === null) {
|
|
100
|
+
throw gateError("confirm");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Resolve refs and validate directories up front; missing ones become
|
|
104
|
+
// FAILED sections instead of aborting the whole batch.
|
|
105
|
+
const active = readActive(workdir);
|
|
106
|
+
const jobs: AnalysisJob[] = params.refs.map((ref, index) => {
|
|
107
|
+
const dir = path.resolve(workdir, ref.localPath.replace(/^@/, ""));
|
|
108
|
+
const missing = fs.existsSync(dir) && fs.statSync(dir).isDirectory() ? null : `reference directory not found: ${dir}`;
|
|
109
|
+
return {
|
|
110
|
+
input: { refId: ref.id, localPath: dir, title: ref.title, url: ref.url, kind: ref.kind, context: params.context },
|
|
111
|
+
name: `pi-plans-refs-${active?.run_id ?? "adhoc"}-ref-${index + 1}`,
|
|
112
|
+
laneId: `ref-${index + 1}`,
|
|
113
|
+
dir,
|
|
114
|
+
missing,
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
if (jobs.length === 0) {
|
|
118
|
+
throw new StateError("analyze_refs requires at least one reference");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const model = reviewer.model_selector ?? (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined);
|
|
122
|
+
const modelLabel = model ?? "inherit";
|
|
123
|
+
let languageTag: string | null = null;
|
|
124
|
+
if (active) {
|
|
125
|
+
try {
|
|
126
|
+
const run = JSON.parse(fs.readFileSync(path.join(root, "runs", active.run_id, "run.json"), "utf8")) as { language_tag?: string | null };
|
|
127
|
+
languageTag = run.language_tag ?? null;
|
|
128
|
+
} catch {
|
|
129
|
+
languageTag = null; // best-effort: corrupt/missing run.json must not fail the call
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const record = (name: string, okModel?: string | null) => {
|
|
134
|
+
if (!active) return;
|
|
135
|
+
try {
|
|
136
|
+
recordSubagent(workdir, active.run_id, { role: "ref-analyst", name, model: okModel ?? model ?? null });
|
|
137
|
+
} catch {
|
|
138
|
+
/* best-effort: audit survives in the tool result */
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const runJob = async (job: AnalysisJob, overlay: RefineOverlayController | undefined, relay: AbortController) => {
|
|
143
|
+
if (job.missing) {
|
|
144
|
+
return { ok: false as const, output: "", model: undefined, errorMessage: job.missing, stderr: "", turns: 0 };
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const result = await runPiSubagent({
|
|
148
|
+
systemPrompt: agentPrompt,
|
|
149
|
+
task: buildRefAnalystTask({ ...job.input, languageTag }),
|
|
150
|
+
cwd: job.dir,
|
|
151
|
+
model,
|
|
152
|
+
tools: READ_ONLY_TOOLS,
|
|
153
|
+
signal: relay.signal,
|
|
154
|
+
onProgress: (event) => overlay?.update(job.laneId, event),
|
|
155
|
+
});
|
|
156
|
+
overlay?.complete(job.laneId, result);
|
|
157
|
+
record(job.name, result.ok ? result.model ?? model : null);
|
|
158
|
+
return result;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
record(job.name, null);
|
|
161
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
162
|
+
const result = { ok: false as const, output: "", model: model ?? undefined, errorMessage: message, stderr: "", turns: 0 };
|
|
163
|
+
overlay?.complete(job.laneId, result);
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const sections: string[] = [];
|
|
169
|
+
const outputs: Array<{ name: string; lane: string; refId: string; ok: boolean; output: string; errorMessage?: string; turns: number }> = [];
|
|
170
|
+
let failures = 0;
|
|
171
|
+
|
|
172
|
+
// Sequential batches of at most BATCH_SIZE lanes; each batch gets its
|
|
173
|
+
// own overlay with the same lifecycle as a single refine round.
|
|
174
|
+
for (let start = 0; start < jobs.length; start += BATCH_SIZE) {
|
|
175
|
+
const batch = jobs.slice(start, start + BATCH_SIZE);
|
|
176
|
+
const controller = new AbortController();
|
|
177
|
+
const relayAbort = () => controller.abort();
|
|
178
|
+
if (signal?.aborted) controller.abort();
|
|
179
|
+
else signal?.addEventListener("abort", relayAbort, { once: true });
|
|
180
|
+
|
|
181
|
+
const overlay =
|
|
182
|
+
ctx.mode === "tui"
|
|
183
|
+
? new RefineOverlayController("refs", batch.map((job) => ({ id: job.laneId, label: job.laneId })), relayAbort)
|
|
184
|
+
: undefined;
|
|
185
|
+
overlay?.open(refineOverlayContext(ctx), modelLabel);
|
|
186
|
+
try {
|
|
187
|
+
const results = await Promise.all(batch.map((job) => runJob(job, overlay, controller)));
|
|
188
|
+
for (let i = 0; i < batch.length; i += 1) {
|
|
189
|
+
const job = batch[i]!;
|
|
190
|
+
const result = results[i]!;
|
|
191
|
+
const title = `${job.name} — ${job.input.title ?? job.input.refId}`;
|
|
192
|
+
if (!result.ok) {
|
|
193
|
+
failures += 1;
|
|
194
|
+
sections.push(`### ${title} — FAILED\n${result.errorMessage ?? "unknown error"}`);
|
|
195
|
+
} else {
|
|
196
|
+
sections.push(`### ${title}\n${result.output}`);
|
|
197
|
+
}
|
|
198
|
+
outputs.push({
|
|
199
|
+
name: job.name,
|
|
200
|
+
lane: job.laneId,
|
|
201
|
+
refId: job.input.refId,
|
|
202
|
+
ok: result.ok,
|
|
203
|
+
output: result.output,
|
|
204
|
+
errorMessage: result.errorMessage,
|
|
205
|
+
turns: result.turns,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
} finally {
|
|
209
|
+
await overlay?.close();
|
|
210
|
+
signal?.removeEventListener("abort", relayAbort);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (failures === jobs.length) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`all reference analysis subagents failed (${failures}/${jobs.length})${model ? `\nIf the model selector "${model}" is unavailable, reset the reviewer confirmation (plans set-role, role=reviewer, resetConfirmation: true) and re-ask the model-confirmation question.` : ""}`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const combined = sections.join("\n\n---\n\n");
|
|
221
|
+
const truncation = truncateHead(combined, { maxLines: 2000, maxBytes: 50 * 1024 });
|
|
222
|
+
let text = truncation.content;
|
|
223
|
+
if (truncation.truncated) text += `\n\n[Output truncated; full outputs remain in this tool result's details.]`;
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
content: [
|
|
227
|
+
{
|
|
228
|
+
type: "text",
|
|
229
|
+
text: `${text}\n\n---\nPersist: paste each reference's analysis into REF_ANALYSIS.md, call the plans tool (record-ref) per reference with coverage and gaps filled from the analysis, then ask at least three ref-specific adoption questions per reference with ask_choice before using its ideas in PLAN_v1.md.`,
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
details: {
|
|
233
|
+
mode: "delegated-subagent",
|
|
234
|
+
role: "ref-analyst",
|
|
235
|
+
reviewerGates: { mode: reviewer.mode, model },
|
|
236
|
+
batches: Math.ceil(jobs.length / BATCH_SIZE),
|
|
237
|
+
model,
|
|
238
|
+
outputs,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
renderCall(args, theme) {
|
|
244
|
+
const count = args.refs?.length ?? 0;
|
|
245
|
+
let text = theme.fg("toolTitle", theme.bold("analyze_refs ")) + theme.fg("accent", `${count} ref${count === 1 ? "" : "s"}`);
|
|
246
|
+
const first = args.refs?.[0]?.localPath;
|
|
247
|
+
if (first) text += theme.fg("dim", ` ${first.split("/").pop() ?? first}${count > 1 ? ` +${count - 1}` : ""}`);
|
|
248
|
+
return new Text(text, 0, 0);
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
renderResult(result, { expanded }, theme) {
|
|
252
|
+
const text = result.content[0];
|
|
253
|
+
const raw = text?.type === "text" ? text.text : "";
|
|
254
|
+
if (!expanded) {
|
|
255
|
+
const firstLine = raw.split("\n").find((line) => line.trim()) ?? "(no output)";
|
|
256
|
+
return new Text(theme.fg("success", "✓ ") + theme.fg("muted", firstLine.slice(0, 120)), 0, 0);
|
|
257
|
+
}
|
|
258
|
+
return new Text(raw, 0, 0);
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export type { ExtensionContext };
|
package/tools/ask-choice.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
15
15
|
import { Text } from "@earendil-works/pi-tui";
|
|
16
16
|
import { Type } from "typebox";
|
|
17
17
|
import { disableAutoComplete, enableAutoComplete, isAutoCompleteEnabled, recordAskChoice } from "../src/autocomplete.ts";
|
|
18
|
+
import { TERMINATION_QUESTION, TERMINATION_OPTIONS, renderTerminationOptions } from "../src/termination-prompt.ts";
|
|
18
19
|
import { truncateToWidth, visibleWidth } from "../src/refine-ui-helpers.ts";
|
|
19
20
|
import { normalizeWorkdir, readActive, recordDecision } from "../src/state.ts";
|
|
20
21
|
|
|
@@ -298,7 +299,7 @@ export function registerAskChoiceTool(pi: ExtensionAPI): void {
|
|
|
298
299
|
content: [
|
|
299
300
|
{
|
|
300
301
|
type: "text",
|
|
301
|
-
text: `User selected Auto-refine loop. Immediately ask the follow-up with ask_choice (autoComplete: false, in the session language):
|
|
302
|
+
text: `User selected Auto-refine loop. Immediately ask the follow-up with ask_choice (autoComplete: false, in the session language): "${TERMINATION_QUESTION}" Options (recommended first): ${renderTerminationOptions()}. Then run the loop per the completion instructions: each round calls refine (role: "reviewer", target: "implementation"), accepts findings on evidence, applies fixes, re-runs relevant tests, and continues until the chosen termination condition — the goal-wait option keeps the loop running until no unpassed VCs remain.`,
|
|
302
303
|
},
|
|
303
304
|
],
|
|
304
305
|
details: details("Auto-refine loop", "user"),
|
package/tools/code-graph.ts
CHANGED
|
@@ -21,6 +21,8 @@ import { hashText } from "../src/code-graph/parser.ts";
|
|
|
21
21
|
import type { Language } from "../src/code-graph/types.ts";
|
|
22
22
|
import { screeningQuery } from "../src/code-graph/screening.ts";
|
|
23
23
|
import { deleteFile, listPending, updateFile, updateFunction } from "../src/code-graph/mutations.ts";
|
|
24
|
+
import { applyGraphCore } from "../src/code-graph/commands.ts";
|
|
25
|
+
import { normalizeWorkdir } from "../src/state.ts";
|
|
24
26
|
|
|
25
27
|
const CodeGraphParams = Type.Object({
|
|
26
28
|
action: StringEnum(
|
|
@@ -34,6 +36,7 @@ const CodeGraphParams = Type.Object({
|
|
|
34
36
|
"list-pending",
|
|
35
37
|
"reindex",
|
|
36
38
|
"manifest",
|
|
39
|
+
"apply",
|
|
37
40
|
] as const,
|
|
38
41
|
{ description: "Code graph action to perform" },
|
|
39
42
|
),
|
|
@@ -45,7 +48,6 @@ const CodeGraphParams = Type.Object({
|
|
|
45
48
|
fullCode: Type.Optional(Type.String({ description: "New function body text for update-function" })),
|
|
46
49
|
text: Type.Optional(Type.String({ description: "New whole-file text for update-file" })),
|
|
47
50
|
limit: Type.Optional(Type.Number()),
|
|
48
|
-
force: Type.Optional(Type.Boolean()),
|
|
49
51
|
});
|
|
50
52
|
|
|
51
53
|
export type CodeGraphContext = Parameters<Parameters<ExtensionAPI["registerTool"]>[0]["execute"]>[4];
|
|
@@ -96,11 +98,32 @@ export function registerCodeGraphTool(pi: ExtensionAPI): void {
|
|
|
96
98
|
name: "code_graph",
|
|
97
99
|
label: "Code Graph",
|
|
98
100
|
description:
|
|
99
|
-
"
|
|
101
|
+
"Code-graph actions: read-only queries (status, screening without full_code, function read, manifest summary) plus `apply` — safe non-force materialization of DB-first staged edits into the worktree (same gate as /apply-graph: refused during active planning/accepted runs and for read-only refiner subagents via PI_PLANS_REFINER; returns per-file report with counts and a post-apply drift summary; never changes run status). reindex stays user-only (/init-graph).",
|
|
100
102
|
promptSnippet: "Read-only code graph queries",
|
|
101
103
|
parameters: CodeGraphParams,
|
|
102
104
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
103
105
|
const workdir = params.workdir ?? ctx.cwd;
|
|
106
|
+
if (params.action === "apply") {
|
|
107
|
+
if (process.env.PI_PLANS_REFINER === "1") {
|
|
108
|
+
return {
|
|
109
|
+
content: [{ type: "text", text: JSON.stringify({ ok: false, reason: "code-graph apply refused: read-only refiner subagents cannot materialize worktree edits (PI_PLANS_REFINER)" }) }],
|
|
110
|
+
details: {},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const core = await applyGraphCore(normalizeWorkdir(workdir));
|
|
114
|
+
if (core.refused || core.failed || !core.report) {
|
|
115
|
+
return {
|
|
116
|
+
content: [{ type: "text", text: JSON.stringify({ ok: false, reason: core.refused ?? core.failed ?? "unknown error" }) }],
|
|
117
|
+
details: {},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const counts: Record<string, number> = { ok: 0, deleted: 0, stale: 0, "skipped-missing": 0, error: 0 };
|
|
121
|
+
for (const file of core.report.files) counts[file.status] = (counts[file.status] ?? 0) + 1;
|
|
122
|
+
return {
|
|
123
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, report: { counts, files: core.report.files }, drift: core.drift ?? null }) }],
|
|
124
|
+
details: {},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
104
127
|
const ensured = await ensureRuntime(workdir, ctx);
|
|
105
128
|
if (!ensured) {
|
|
106
129
|
return {
|
package/tools/execute-plan.ts
CHANGED
|
@@ -9,6 +9,8 @@ import { Type } from "typebox";
|
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
import {
|
|
12
|
+
getExecution,
|
|
13
|
+
resumeActiveExecution,
|
|
12
14
|
startExecution,
|
|
13
15
|
} from "../src/exec.ts";
|
|
14
16
|
import { disableAutoComplete } from "../src/autocomplete.ts";
|
|
@@ -97,6 +99,22 @@ export async function executeHandoff(
|
|
|
97
99
|
};
|
|
98
100
|
}
|
|
99
101
|
|
|
102
|
+
/** The user command may resume an approved execution; the tool always asks. */
|
|
103
|
+
export async function executeCommand(ctx: ExtensionContext, planPathArg?: string): Promise<HandoffOutcome> {
|
|
104
|
+
const activeExecution = getExecution();
|
|
105
|
+
const planPath = planPathArg ? path.resolve(ctx.cwd, planPathArg.replace(/^@/, "")) : activeExecution?.planPath;
|
|
106
|
+
if (activeExecution && planPath && path.resolve(activeExecution.planPath) === path.resolve(planPath)) {
|
|
107
|
+
const resumed = resumeActiveExecution(getCurrentApi(), ctx);
|
|
108
|
+
return {
|
|
109
|
+
status: "executing",
|
|
110
|
+
planPath,
|
|
111
|
+
itemCount: activeExecution.items.length,
|
|
112
|
+
message: resumed ? "Execution resumed; verified progress preserved." : "This plan is already executing.",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return executeHandoff(ctx, planPathArg);
|
|
116
|
+
}
|
|
117
|
+
|
|
100
118
|
// The tool registers with the ExtensionAPI in scope; keep a module-level
|
|
101
119
|
// reference so the shared handoff helper can reach appendEntry/sendMessage.
|
|
102
120
|
let currentApi: ExtensionAPI | null = null;
|
|
@@ -302,7 +302,7 @@ function createGraphWriteTool(cwd: string) {
|
|
|
302
302
|
content: [
|
|
303
303
|
{
|
|
304
304
|
type: "text",
|
|
305
|
-
text: `code graph: staged ${mutation.created ? "new" : "updated"} file ${info.relativePath}; run
|
|
305
|
+
text: `code graph: staged ${mutation.created ? "new" : "updated"} file ${info.relativePath}; run code_graph apply to materialize`,
|
|
306
306
|
},
|
|
307
307
|
],
|
|
308
308
|
details: {},
|
|
@@ -363,7 +363,7 @@ function createGraphEditTool(cwd: string) {
|
|
|
363
363
|
content: [
|
|
364
364
|
{
|
|
365
365
|
type: "text",
|
|
366
|
-
text: `${textContent(result)} (staged in code graph; run
|
|
366
|
+
text: `${textContent(result)} (staged in code graph; run code_graph apply to materialize)`,
|
|
367
367
|
},
|
|
368
368
|
],
|
|
369
369
|
};
|
package/tools/plans.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
setArtifactRoot,
|
|
23
23
|
setGraphEnabled,
|
|
24
24
|
setLanguage,
|
|
25
|
+
setRefsRoot,
|
|
25
26
|
setRunStatus,
|
|
26
27
|
setRole,
|
|
27
28
|
showConfig,
|
|
@@ -37,6 +38,7 @@ const PlansParams = Type.Object({
|
|
|
37
38
|
"show",
|
|
38
39
|
"set-language",
|
|
39
40
|
"set-artifact-root",
|
|
41
|
+
"set-refs-root",
|
|
40
42
|
"set-graph-enabled",
|
|
41
43
|
"set-role",
|
|
42
44
|
"start-run",
|
|
@@ -58,6 +60,8 @@ const PlansParams = Type.Object({
|
|
|
58
60
|
languageSource: Type.Optional(StringEnum(["user", "auto"] as const)),
|
|
59
61
|
artifactRoot: Type.Optional(Type.String({ description: "set-artifact-root: planning docs root, e.g. ./docs/pi-plans" })),
|
|
60
62
|
artifactRootSource: Type.Optional(StringEnum(["user", "auto"] as const)),
|
|
63
|
+
refsRoot: Type.Optional(Type.String({ description: "set-refs-root: reference downloads root, e.g. .git/pi-plans/refs" })),
|
|
64
|
+
refsRootSource: Type.Optional(StringEnum(["user", "auto"] as const)),
|
|
61
65
|
enabled: Type.Optional(Type.Boolean({ description: "set-graph-enabled: enable/disable the code graph" })),
|
|
62
66
|
message: Type.Optional(Type.String({ description: "final-commit: commit message body" })),
|
|
63
67
|
role: Type.Optional(StringEnum(["reviewer", "criticizer"] as const)),
|
|
@@ -98,7 +102,7 @@ const PlansParams = Type.Object({
|
|
|
98
102
|
),
|
|
99
103
|
subagent: Type.Optional(
|
|
100
104
|
Type.Object({
|
|
101
|
-
role: StringEnum(["reviewer", "criticizer"] as const),
|
|
105
|
+
role: StringEnum(["reviewer", "criticizer", "ref-analyst"] as const),
|
|
102
106
|
name: Type.String(),
|
|
103
107
|
model: Type.Optional(Type.String()),
|
|
104
108
|
sessionDir: Type.Optional(Type.String()),
|
|
@@ -172,7 +176,7 @@ export function registerPlansTool(pi: ExtensionAPI): void {
|
|
|
172
176
|
name: "plans",
|
|
173
177
|
label: "Plans",
|
|
174
178
|
description:
|
|
175
|
-
"Manage pi-plans planning state in the target workspace: init/show config, set language and planning docs root plus reviewer/criticizer roles and the code-graph enabled flag, start planning runs, record decisions/refs/subagents, and update run status. State lives in .git/pi_plans/ inside the resolved git common dir. Actions: init, show, set-language, set-artifact-root, set-graph-enabled, set-role, start-run, set-status, final-commit, record-decision, record-ref, record-subagent.",
|
|
179
|
+
"Manage pi-plans planning state in the target workspace: init/show config, set language and planning docs root plus reviewer/criticizer roles and the code-graph enabled flag, start planning runs, record decisions/refs/subagents, and update run status. State lives in .git/pi_plans/ inside the resolved git common dir. Actions: init, show, set-language, set-artifact-root, set-refs-root, set-graph-enabled, set-role, start-run, set-status, final-commit, record-decision, record-ref, record-subagent.",
|
|
176
180
|
promptSnippet: "Manage pi-plans planning state, runs, and ledgers",
|
|
177
181
|
parameters: PlansParams,
|
|
178
182
|
|
|
@@ -228,6 +232,14 @@ export function registerPlansTool(pi: ExtensionAPI): void {
|
|
|
228
232
|
result = { config: updated.config, stateRoot: updated.stateRoot, notices: updated.notices };
|
|
229
233
|
break;
|
|
230
234
|
}
|
|
235
|
+
case "set-refs-root": {
|
|
236
|
+
if (!params.refsRoot || !params.refsRootSource) {
|
|
237
|
+
throw new StateError("set-refs-root requires refsRoot and refsRootSource");
|
|
238
|
+
}
|
|
239
|
+
const updated = setRefsRoot(workdir, params.refsRoot, params.refsRootSource);
|
|
240
|
+
result = { config: updated.config, stateRoot: updated.stateRoot, notices: updated.notices };
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
231
243
|
case "set-role": {
|
|
232
244
|
if (!params.role) throw new StateError("set-role requires role");
|
|
233
245
|
const updated = setRole(workdir, {
|