pi-plans 0.1.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/LICENSE +21 -0
- package/README.md +217 -0
- package/agents/criticizer.md +18 -0
- package/agents/reviewer.md +20 -0
- package/docs/assets/pi-plans-logo.svg +66 -0
- package/index.ts +375 -0
- package/package.json +58 -0
- package/references/pi-planning-workflow.md +154 -0
- package/references/plan-artifact-template.md +77 -0
- package/references/state-and-config.md +137 -0
- package/scripts/run-tests.ts +31 -0
- package/scripts/validate.ts +185 -0
- package/skills/debug-and-plan/SKILL.md +35 -0
- package/skills/plan-big/SKILL.md +24 -0
- package/skills/plan-normal/SKILL.md +24 -0
- package/skills/plan-small/SKILL.md +23 -0
- package/skills/plan-with-refs/SKILL.md +30 -0
- package/skills/planning/SKILL.md +22 -0
- package/src/exec.ts +317 -0
- package/src/execution-panel.ts +497 -0
- package/src/guard.ts +38 -0
- package/src/plan.ts +63 -0
- package/src/refine-prompts.ts +70 -0
- package/src/state.ts +490 -0
- package/src/subagent.ts +197 -0
- package/tests/exec.test.ts +249 -0
- package/tests/execution-panel.test.ts +198 -0
- package/tests/guard.test.ts +70 -0
- package/tests/plan.test.ts +105 -0
- package/tests/refine-prompts.test.ts +49 -0
- package/tests/state.test.ts +240 -0
- package/tools/ask-choice.ts +199 -0
- package/tools/execute-plan.ts +137 -0
- package/tools/plans.ts +195 -0
- package/tools/refine.ts +237 -0
package/tools/plans.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `plans` tool — the state CLI for the pi-plans workflow, exposed as one
|
|
3
|
+
* typed tool.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
7
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { Type } from "typebox";
|
|
9
|
+
import {
|
|
10
|
+
initState,
|
|
11
|
+
loadConfig,
|
|
12
|
+
normalizeWorkdir,
|
|
13
|
+
recordDecision,
|
|
14
|
+
recordRef,
|
|
15
|
+
recordSubagent,
|
|
16
|
+
resolveStateRootOrNull,
|
|
17
|
+
setArtifactRoot,
|
|
18
|
+
setLanguage,
|
|
19
|
+
setRunStatus,
|
|
20
|
+
setRole,
|
|
21
|
+
showConfig,
|
|
22
|
+
startRun,
|
|
23
|
+
StateError,
|
|
24
|
+
VALID_RUN_STATUSES,
|
|
25
|
+
} from "../src/state.ts";
|
|
26
|
+
|
|
27
|
+
const PlansParams = Type.Object({
|
|
28
|
+
action: StringEnum(
|
|
29
|
+
[
|
|
30
|
+
"init",
|
|
31
|
+
"show",
|
|
32
|
+
"set-language",
|
|
33
|
+
"set-artifact-root",
|
|
34
|
+
"set-role",
|
|
35
|
+
"start-run",
|
|
36
|
+
"set-status",
|
|
37
|
+
"record-decision",
|
|
38
|
+
"record-ref",
|
|
39
|
+
"record-subagent",
|
|
40
|
+
] as const,
|
|
41
|
+
{ description: "State command to run" },
|
|
42
|
+
),
|
|
43
|
+
workdir: Type.Optional(
|
|
44
|
+
Type.String({ description: "Target workspace directory. Default: current working directory." }),
|
|
45
|
+
),
|
|
46
|
+
topic: Type.Optional(Type.String({ description: "start-run: short topic slug for the run" })),
|
|
47
|
+
skill: Type.Optional(Type.String({ description: "start-run: skill name starting the run" })),
|
|
48
|
+
requestText: Type.Optional(Type.String({ description: "start-run: original user request text" })),
|
|
49
|
+
tag: Type.Optional(Type.String({ description: "set-language: BCP47 tag, e.g. zh-Hans, en" })),
|
|
50
|
+
languageSource: Type.Optional(StringEnum(["user", "auto"] as const)),
|
|
51
|
+
artifactRoot: Type.Optional(Type.String({ description: "set-artifact-root: planning docs root, e.g. ./docs/pi-plans" })),
|
|
52
|
+
artifactRootSource: Type.Optional(StringEnum(["user", "auto"] as const)),
|
|
53
|
+
role: Type.Optional(StringEnum(["reviewer", "criticizer"] as const)),
|
|
54
|
+
mode: Type.Optional(StringEnum(["delegated-subagent", "current-session"] as const)),
|
|
55
|
+
modelSelector: Type.Optional(
|
|
56
|
+
Type.String({ description: "set-role: exact provider/model selector, or 'inherit' to reset to inherited" }),
|
|
57
|
+
),
|
|
58
|
+
confirmed: Type.Optional(
|
|
59
|
+
Type.Boolean({ description: "set-role: stamp confirmed_at=now (used by the first-use confirmation flow)" }),
|
|
60
|
+
),
|
|
61
|
+
resetConfirmation: Type.Optional(Type.Boolean({ description: "set-role: clear confirmed_at to re-ask" })),
|
|
62
|
+
runId: Type.Optional(Type.String()),
|
|
63
|
+
status: Type.Optional(
|
|
64
|
+
StringEnum(
|
|
65
|
+
["planning", "accepted", "executing", "stopped", "abandoned", "done"] as const,
|
|
66
|
+
{ description: "set-status: run lifecycle status" },
|
|
67
|
+
),
|
|
68
|
+
),
|
|
69
|
+
decision: Type.Optional(
|
|
70
|
+
Type.Object({
|
|
71
|
+
question: Type.String(),
|
|
72
|
+
options: Type.Array(Type.String()),
|
|
73
|
+
answer: Type.String(),
|
|
74
|
+
answerSource: StringEnum(["user", "auto-complete"] as const),
|
|
75
|
+
artifact: Type.Optional(Type.String()),
|
|
76
|
+
}),
|
|
77
|
+
),
|
|
78
|
+
ref: Type.Optional(
|
|
79
|
+
Type.Object({
|
|
80
|
+
title: Type.String(),
|
|
81
|
+
url: Type.String(),
|
|
82
|
+
kind: Type.String(),
|
|
83
|
+
retrieval: Type.String(),
|
|
84
|
+
localPath: Type.Optional(Type.String()),
|
|
85
|
+
coverage: Type.Optional(Type.String()),
|
|
86
|
+
gaps: Type.Optional(Type.String()),
|
|
87
|
+
}),
|
|
88
|
+
),
|
|
89
|
+
subagent: Type.Optional(
|
|
90
|
+
Type.Object({
|
|
91
|
+
role: StringEnum(["reviewer", "criticizer"] as const),
|
|
92
|
+
name: Type.String(),
|
|
93
|
+
model: Type.Optional(Type.String()),
|
|
94
|
+
sessionDir: Type.Optional(Type.String()),
|
|
95
|
+
}),
|
|
96
|
+
),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
export function registerPlansTool(pi: ExtensionAPI): void {
|
|
100
|
+
pi.registerTool({
|
|
101
|
+
name: "plans",
|
|
102
|
+
label: "Plans",
|
|
103
|
+
description:
|
|
104
|
+
"Manage pi-plans planning state in the target workspace: init/show config, set language and planning docs root plus reviewer/criticizer roles, 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-role, start-run, set-status, record-decision, record-ref, record-subagent.",
|
|
105
|
+
promptSnippet: "Manage pi-plans planning state, runs, and ledgers",
|
|
106
|
+
parameters: PlansParams,
|
|
107
|
+
|
|
108
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
109
|
+
const workdir = normalizeWorkdir(params.workdir ?? ctx.cwd);
|
|
110
|
+
try {
|
|
111
|
+
let result: unknown;
|
|
112
|
+
switch (params.action) {
|
|
113
|
+
case "init": {
|
|
114
|
+
const ensured = initState(workdir);
|
|
115
|
+
result = { config: ensured.config, stateRoot: ensured.stateRoot, notices: ensured.notices };
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case "show": {
|
|
119
|
+
const config = showConfig(workdir);
|
|
120
|
+
const stateRoot = resolveStateRootOrNull(workdir);
|
|
121
|
+
result = { config, stateRoot };
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
case "set-language": {
|
|
125
|
+
if (!params.tag || !params.languageSource) {
|
|
126
|
+
throw new StateError("set-language requires tag and languageSource");
|
|
127
|
+
}
|
|
128
|
+
const updated = setLanguage(workdir, params.tag, params.languageSource);
|
|
129
|
+
result = { config: updated.config, stateRoot: updated.stateRoot, notices: updated.notices };
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case "set-artifact-root": {
|
|
133
|
+
if (!params.artifactRoot || !params.artifactRootSource) {
|
|
134
|
+
throw new StateError("set-artifact-root requires artifactRoot and artifactRootSource");
|
|
135
|
+
}
|
|
136
|
+
const updated = setArtifactRoot(workdir, params.artifactRoot, params.artifactRootSource);
|
|
137
|
+
result = { config: updated.config, stateRoot: updated.stateRoot, notices: updated.notices };
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case "set-role": {
|
|
141
|
+
if (!params.role) throw new StateError("set-role requires role");
|
|
142
|
+
const updated = setRole(workdir, {
|
|
143
|
+
role: params.role,
|
|
144
|
+
mode: params.mode,
|
|
145
|
+
modelSelector: params.modelSelector,
|
|
146
|
+
confirmed: params.confirmed,
|
|
147
|
+
resetConfirmation: params.resetConfirmation,
|
|
148
|
+
});
|
|
149
|
+
result = { config: updated.config, stateRoot: updated.stateRoot, notices: updated.notices };
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case "start-run": {
|
|
153
|
+
if (!params.topic || !params.skill || !params.requestText) {
|
|
154
|
+
throw new StateError("start-run requires topic, skill, and requestText");
|
|
155
|
+
}
|
|
156
|
+
result = startRun(workdir, {
|
|
157
|
+
topic: params.topic,
|
|
158
|
+
skill: params.skill,
|
|
159
|
+
requestText: params.requestText,
|
|
160
|
+
});
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
case "set-status": {
|
|
164
|
+
if (!params.runId || !params.status) throw new StateError("set-status requires runId and status");
|
|
165
|
+
result = setRunStatus(workdir, params.runId, params.status);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
case "record-decision": {
|
|
169
|
+
if (!params.runId || !params.decision) {
|
|
170
|
+
throw new StateError("record-decision requires runId and decision");
|
|
171
|
+
}
|
|
172
|
+
result = recordDecision(workdir, params.runId, params.decision);
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
case "record-ref": {
|
|
176
|
+
if (!params.runId || !params.ref) throw new StateError("record-ref requires runId and ref");
|
|
177
|
+
result = recordRef(workdir, params.runId, params.ref);
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
case "record-subagent": {
|
|
181
|
+
if (!params.runId || !params.subagent) {
|
|
182
|
+
throw new StateError("record-subagent requires runId and subagent");
|
|
183
|
+
}
|
|
184
|
+
result = recordSubagent(workdir, params.runId, params.subagent);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: {} };
|
|
189
|
+
} catch (error) {
|
|
190
|
+
// Surface state errors as tool errors so the model sees the guidance.
|
|
191
|
+
throw new Error(`pi-plans ${params.action} failed: ${(error as Error).message}`);
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
package/tools/refine.ts
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `refine` tool — reviewer/criticizer refinement rounds via read-only Pi
|
|
3
|
+
* subagents with isolated context.
|
|
4
|
+
*
|
|
5
|
+
* Enforces the role-confirmation gate: refuses to spawn while a
|
|
6
|
+
* role's mode is invalid or its model was never confirmed, telling the caller
|
|
7
|
+
* exactly which ask_choice question to ask first.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
11
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { truncateHead } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
14
|
+
import { Type } from "typebox";
|
|
15
|
+
import * as fs from "node:fs";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import { loadConfig, normalizeWorkdir, readActive, recordSubagent, resolveStateRootOrNull, StateError, type RoleConfig } from "../src/state.ts";
|
|
18
|
+
import { buildCriticizerTask, buildReviewerTask, reviewerLanes } from "../src/refine-prompts.ts";
|
|
19
|
+
import { runPiSubagent, stripFrontmatter } from "../src/subagent.ts";
|
|
20
|
+
|
|
21
|
+
const RefineParams = Type.Object({
|
|
22
|
+
role: StringEnum(["reviewer", "criticizer"] as const, { description: "Refinement role to run" }),
|
|
23
|
+
planPath: Type.String({ description: "Path to the PLAN_vN.md to review (absolute or relative to workdir)" }),
|
|
24
|
+
focus: Type.Optional(Type.String({ description: "Specific concerns to direct the pass at" })),
|
|
25
|
+
reviewers: Type.Optional(
|
|
26
|
+
Type.Integer({
|
|
27
|
+
minimum: 1,
|
|
28
|
+
maximum: 3,
|
|
29
|
+
description: "Number of independent reviewer subagents (big plans: 3 for the concurrent round). Criticizer is always 1.",
|
|
30
|
+
}),
|
|
31
|
+
),
|
|
32
|
+
context: Type.Optional(
|
|
33
|
+
Type.String({ description: "Context for the subagents: user goals, repo evidence, constraints, open questions" }),
|
|
34
|
+
),
|
|
35
|
+
workdir: Type.Optional(Type.String({ description: "Target workspace; default current working directory" })),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function roleGateError(role: string, roleConfig: RoleConfig | undefined, problem: "mode" | "confirm"): StateError {
|
|
39
|
+
if (problem === "mode") {
|
|
40
|
+
return new StateError(
|
|
41
|
+
`The ${role} role mode is missing or invalid in .git/pi_plans/config.json. Ask the role-setting question with ask_choice first: 1. Delegated subagent (recommended; read-only pi subprocess with isolated context) 2. Current session (run the pass yourself in this session) 3. Other 4. Auto-complete — then persist with the plans tool (set-role).`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return new StateError(
|
|
45
|
+
`The ${role} model was never confirmed (confirmed_at is null). 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, confirmed: true, modelSelector: the selector or 'inherit').`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
export function registerRefineTool(pi: ExtensionAPI, baseDir: string): void {
|
|
52
|
+
const agentsDir = path.join(baseDir, "agents");
|
|
53
|
+
|
|
54
|
+
const loadAgentPrompt = (role: "reviewer" | "criticizer"): string => {
|
|
55
|
+
const file = path.join(agentsDir, `${role}.md`);
|
|
56
|
+
return stripFrontmatter(fs.readFileSync(file, "utf8"));
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
pi.registerTool({
|
|
60
|
+
name: "refine",
|
|
61
|
+
label: "Refine",
|
|
62
|
+
description:
|
|
63
|
+
"Run a reviewer or criticizer refinement round on a PLAN_vN.md via read-only Pi subagents. Reviewer: findings with IDs, severity, evidence, impact, fix, disposition. Criticizer: up to five adaptive questions. Use reviewers: 3 for the big-plan concurrent reviewer round. Refuses to spawn until the role's mode and model are confirmed in .git/pi_plans/config.json (ask via ask_choice, persist via the plans tool).",
|
|
64
|
+
promptSnippet: "Run reviewer/criticizer plan-refinement rounds",
|
|
65
|
+
parameters: RefineParams,
|
|
66
|
+
|
|
67
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
68
|
+
const workdir = normalizeWorkdir(params.workdir ?? ctx.cwd);
|
|
69
|
+
|
|
70
|
+
// Read config read-only; state must already exist.
|
|
71
|
+
const root = resolveStateRootOrNull(workdir);
|
|
72
|
+
if (root === null || !fs.existsSync(path.join(root, "config.json"))) {
|
|
73
|
+
throw new StateError("no pi-plans state found; run the plans tool (action: init) first");
|
|
74
|
+
}
|
|
75
|
+
const config = loadConfig(root);
|
|
76
|
+
const roleConfig = config[params.role] as RoleConfig | undefined;
|
|
77
|
+
if (!roleConfig || (roleConfig.mode !== "delegated-subagent" && roleConfig.mode !== "current-session")) {
|
|
78
|
+
throw roleGateError(params.role, roleConfig, "mode");
|
|
79
|
+
}
|
|
80
|
+
if (roleConfig.confirmed_at === null) {
|
|
81
|
+
throw roleGateError(params.role, roleConfig, "confirm");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Resolve and read the plan.
|
|
85
|
+
const planPath = path.resolve(workdir, params.planPath.replace(/^@/, ""));
|
|
86
|
+
if (!fs.existsSync(planPath)) throw new StateError(`plan file not found: ${planPath}`);
|
|
87
|
+
const planText = fs.readFileSync(planPath, "utf8");
|
|
88
|
+
|
|
89
|
+
// Record spawns against the active run when one exists.
|
|
90
|
+
const active = readActive(workdir);
|
|
91
|
+
const record = (name: string, model?: string | null) => {
|
|
92
|
+
if (!active) return;
|
|
93
|
+
try {
|
|
94
|
+
recordSubagent(workdir, active.run_id, { role: params.role, name, model: model ?? null });
|
|
95
|
+
} catch {
|
|
96
|
+
/* best-effort */
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const systemPrompt = loadAgentPrompt(params.role);
|
|
101
|
+
const inheritModel = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined;
|
|
102
|
+
const model = roleConfig.model_selector ?? inheritModel;
|
|
103
|
+
|
|
104
|
+
if (roleConfig.mode === "current-session") {
|
|
105
|
+
const task =
|
|
106
|
+
params.role === "reviewer"
|
|
107
|
+
? buildReviewerTask({ planText, planPath, lens: null, focus: params.focus, context: params.context })
|
|
108
|
+
: buildCriticizerTask({ planText, planPath, focus: params.focus, context: params.context });
|
|
109
|
+
return {
|
|
110
|
+
content: [
|
|
111
|
+
{
|
|
112
|
+
type: "text",
|
|
113
|
+
text: `Role mode is current-session: perform the read-only ${params.role} pass yourself, in this session, following this brief. Do not spawn anything.\n\n${task}`,
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
details: { mode: "current-session", role: params.role, planPath },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (params.role === "criticizer") {
|
|
121
|
+
const name = `${roleConfig.name_prefix}-criticizer-${Date.now().toString(36)}`;
|
|
122
|
+
const result = await runPiSubagent({
|
|
123
|
+
systemPrompt,
|
|
124
|
+
task: buildCriticizerTask({ planText, planPath, focus: params.focus, context: params.context }),
|
|
125
|
+
cwd: workdir,
|
|
126
|
+
model,
|
|
127
|
+
signal,
|
|
128
|
+
});
|
|
129
|
+
record(name, result.ok ? result.model ?? model : null);
|
|
130
|
+
if (!result.ok) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`criticizer subagent failed: ${result.errorMessage ?? "unknown error"}${result.stderr ? `\nstderr: ${result.stderr.slice(0, 2000)}` : ""}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
content: [
|
|
137
|
+
{
|
|
138
|
+
type: "text",
|
|
139
|
+
text: `${result.output}\n\n---\nAsk each criticizer question with ask_choice (one call per question, in the configured language), record every answer, then revise the plan only after every question has an answer.`,
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
details: { mode: "delegated-subagent", role: params.role, planPath, model: result.model ?? model },
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Reviewer round: 1 by default, 3 for the big-plan concurrent round.
|
|
147
|
+
const count = Math.min(3, Math.max(1, params.reviewers ?? 1));
|
|
148
|
+
const lanes = reviewerLanes(count);
|
|
149
|
+
const jobs = lanes.map((lane) => {
|
|
150
|
+
const name = `${roleConfig.name_prefix}-${active?.run_id ?? "adhoc"}-${lane.id}`;
|
|
151
|
+
const task = buildReviewerTask({ planText, planPath, lens: lane.lens, focus: params.focus, context: params.context });
|
|
152
|
+
return { lane, name, task };
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const results = await Promise.all(
|
|
156
|
+
jobs.map(async (job) => {
|
|
157
|
+
try {
|
|
158
|
+
const result = await runPiSubagent({ systemPrompt, task: job.task, cwd: workdir, model, signal });
|
|
159
|
+
record(job.name, result.ok ? result.model ?? model : null);
|
|
160
|
+
return { job, result };
|
|
161
|
+
} catch (error) {
|
|
162
|
+
record(job.name, null);
|
|
163
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
164
|
+
return {
|
|
165
|
+
job,
|
|
166
|
+
result: { ok: false, output: "", model: model ?? undefined, errorMessage: message, stderr: "", turns: 0 },
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const sections: string[] = [];
|
|
173
|
+
let failures = 0;
|
|
174
|
+
for (const { job, result } of results) {
|
|
175
|
+
const title = job.lane.lens ? `${job.name} — ${job.lane.lens}` : job.name;
|
|
176
|
+
if (!result.ok) {
|
|
177
|
+
failures += 1;
|
|
178
|
+
sections.push(`### ${title} — FAILED\n${result.errorMessage ?? "unknown error"}`);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
sections.push(`### ${title}\n${result.output}`);
|
|
182
|
+
}
|
|
183
|
+
if (failures === results.length) {
|
|
184
|
+
const first = results[0];
|
|
185
|
+
throw new Error(
|
|
186
|
+
`all reviewer subagents failed: ${first?.result.errorMessage ?? "unknown error"}${first?.result.stderr ? `\nstderr: ${first.result.stderr.slice(0, 2000)}` : ""}${model ? `\nIf the model selector "${model}" is unavailable, reset the confirmation (plans set-role --reset-confirmation) and re-ask the model-confirmation question.` : ""}`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const combined = sections.join("\n\n---\n\n");
|
|
191
|
+
const truncation = truncateHead(combined, { maxLines: 2000, maxBytes: 50 * 1024 });
|
|
192
|
+
let text = truncation.content;
|
|
193
|
+
if (truncation.truncated) text += `\n\n[Output truncated; full outputs remain in this tool result's details.]`;
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
content: [
|
|
197
|
+
{
|
|
198
|
+
type: "text",
|
|
199
|
+
text: `${text}\n\n---\nConsolidate: merge and dedupe findings into PLAN_vN_reviewer_comments.md${count === 3 ? " (one consolidated file; keep each finding's source reviewer, severity, evidence, and disposition)" : ""}, accept or reject each finding on repo/reference evidence, surface at most five high-priority findings to the user, then immediately ask the next refinement-mode question with ask_choice.`,
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
details: {
|
|
203
|
+
mode: "delegated-subagent",
|
|
204
|
+
role: "reviewer",
|
|
205
|
+
planPath,
|
|
206
|
+
reviewers: count,
|
|
207
|
+
model,
|
|
208
|
+
outputs: results.map(({ job, result }) => ({ name: job.name, lane: job.lane.id, lens: job.lane.lens, ok: result.ok, output: result.output, stderr: result.stderr, turns: result.turns })),
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
renderCall(args, theme) {
|
|
214
|
+
const count = args.role === "reviewer" ? args.reviewers ?? 1 : 1;
|
|
215
|
+
let text =
|
|
216
|
+
theme.fg("toolTitle", theme.bold("refine ")) +
|
|
217
|
+
theme.fg("accent", args.role) +
|
|
218
|
+
theme.fg("muted", count > 1 ? ` ×${count}` : "");
|
|
219
|
+
const short = args.planPath ? args.planPath.split("/").pop() : "";
|
|
220
|
+
if (short) text += theme.fg("dim", ` ${short}`);
|
|
221
|
+
if (args.focus) text += `\n${theme.fg("dim", ` focus: ${args.focus.slice(0, 80)}`)}`;
|
|
222
|
+
return new Text(text, 0, 0);
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
renderResult(result, { expanded }, theme) {
|
|
226
|
+
const text = result.content[0];
|
|
227
|
+
const raw = text?.type === "text" ? text.text : "";
|
|
228
|
+
if (!expanded) {
|
|
229
|
+
const firstLine = raw.split("\n").find((line) => line.trim()) ?? "(no output)";
|
|
230
|
+
return new Text(theme.fg("success", "✓ ") + theme.fg("muted", firstLine.slice(0, 120)), 0, 0);
|
|
231
|
+
}
|
|
232
|
+
return new Text(raw, 0, 0);
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export type { ExtensionContext };
|