pi-plans 0.6.0 → 0.8.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/AGENTS.md +58 -0
- package/CONTRIBUTING.md +8 -15
- package/README.md +39 -37
- package/agents/execution-reviewer.md +40 -0
- package/agents/reviewer.md +12 -3
- package/index.ts +55 -58
- package/package.json +2 -1
- package/references/pi-planning-workflow.md +50 -60
- package/references/plan-artifact-template.md +81 -60
- package/references/state-and-config.md +60 -44
- package/scripts/bench/pi-adapter/pi_plans_bench.py +33 -22
- package/scripts/bench/pi-adapter/rpc_driver.mjs +4 -4
- package/scripts/run-tests.ts +12 -1
- package/scripts/validate.ts +39 -11
- package/skills/debug-and-plan/SKILL.md +3 -3
- package/skills/plan-big/SKILL.md +3 -3
- package/skills/plan-normal/SKILL.md +3 -3
- package/skills/plan-small/SKILL.md +4 -4
- package/skills/plan-with-refs/SKILL.md +6 -6
- package/skills/planning/SKILL.md +1 -1
- package/src/ask-form.ts +4 -4
- package/src/auditor.ts +227 -0
- package/src/auto-approve.ts +1 -1
- package/src/autocomplete.ts +19 -17
- package/src/code-graph/commands.ts +8 -3
- package/src/code-graph/community.ts +1 -1
- package/src/code-graph/paths.ts +1 -1
- package/src/code-graph/watch.ts +2 -2
- package/src/compaction.ts +3 -3
- package/src/config-command.ts +146 -73
- package/src/dashboard.ts +303 -0
- package/src/exec.ts +1185 -924
- package/src/global-state.ts +304 -0
- package/src/guard.ts +18 -19
- package/src/messaging.ts +44 -0
- package/src/plan.ts +421 -112
- package/src/query-hook.ts +4 -4
- package/src/refine-prompts.ts +12 -70
- package/src/refine-ui-helpers.ts +24 -5
- package/src/refine-ui-state.ts +1 -1
- package/src/refine-ui.ts +19 -3
- package/src/resume-command.ts +45 -129
- package/src/resume.ts +5 -1
- package/src/role-panels.ts +542 -0
- package/src/run-context.ts +3 -10
- package/src/staleness.ts +53 -0
- package/src/state.ts +273 -72
- package/src/subagent.ts +19 -29
- package/src/task-tool.ts +100 -0
- package/src/tasks.ts +223 -0
- package/src/thinking-levels.ts +67 -0
- package/src/ui-language.ts +7 -54
- package/src/workflow-state.ts +76 -58
- package/tests/analyze-refs.test.ts +35 -18
- package/tests/ask-choice-schema.test.ts +0 -12
- package/tests/ask-choice.test.ts +2 -49
- package/tests/ask-form-tool.test.ts +4 -5
- package/tests/ask-form.test.ts +2 -2
- package/tests/auditor.test.ts +210 -0
- package/tests/auto-approve.test.ts +7 -10
- package/tests/autocomplete.test.ts +8 -11
- package/tests/code-graph-apply-action.test.ts +2 -2
- package/tests/code-graph-commands.test.ts +2 -2
- package/tests/code-graph-index.test.ts +2 -2
- package/tests/code-graph-loop.e2e.test.ts +1 -1
- package/tests/code-graph-mutations.test.ts +1 -1
- package/tests/code-graph-rollback.test.ts +1 -1
- package/tests/code-graph-v05.test.ts +2 -2
- package/tests/compaction.test.ts +1 -1
- package/tests/config-command.test.ts +103 -100
- package/tests/dashboard.test.ts +402 -0
- package/tests/exec-lifecycle.test.ts +181 -115
- package/tests/exec-panel-lifecycle.test.ts +106 -251
- package/tests/exec-review-loop.test.ts +331 -0
- package/tests/exec.test.ts +771 -1706
- package/tests/execute-plan.test.ts +44 -19
- package/tests/extension-load.test.ts +48 -0
- package/tests/global-state.test.ts +371 -0
- package/tests/graph-aware-file-tools.test.ts +5 -5
- package/tests/guard.test.ts +1 -1
- package/tests/multi-run.test.ts +3 -103
- package/tests/plan.test.ts +139 -62
- package/tests/plans.test.ts +7 -79
- package/tests/refine-prompts.test.ts +20 -71
- package/tests/refine-resume.test.ts +27 -22
- package/tests/refine-ui.test.ts +6 -15
- package/tests/resume-lifecycle.test.ts +41 -22
- package/tests/resume.test.ts +39 -81
- package/tests/role-panels.test.ts +391 -0
- package/tests/run-context.test.ts +1 -1
- package/tests/run-ownership.test.ts +1 -1
- package/tests/stale-ctx.test.ts +218 -0
- package/tests/staleness.test.ts +76 -0
- package/tests/state.test.ts +155 -32
- package/tests/subagent-thinking.test.ts +65 -0
- package/tests/subagent-usage.test.ts +1 -1
- package/tests/task-tool.test.ts +61 -0
- package/tests/tasks.test.ts +142 -0
- package/tests/thinking-levels.test.ts +77 -0
- package/tests/ui-language.test.ts +2 -17
- package/tests/workflow-state.test.ts +73 -90
- package/tools/analyze-refs.ts +67 -32
- package/tools/ask-choice.ts +7 -53
- package/tools/code-graph.ts +2 -2
- package/tools/execute-plan.ts +55 -99
- package/tools/graph-aware-file-tools.ts +4 -10
- package/tools/plans.ts +41 -67
- package/tools/refine.ts +101 -164
- package/agents/criticizer.md +0 -18
- package/agents/executor.md +0 -26
- package/scripts/bench/pi-adapter/__pycache__/pi_plans_bench.cpython-312.pyc +0 -0
- package/src/panel.ts +0 -473
- package/src/termination-prompt.ts +0 -73
- package/tests/goal-wait.test.ts +0 -269
- package/tests/panel-i-zero.test.ts +0 -420
- package/tests/panel.test.ts +0 -355
package/src/autocomplete.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { getRun, readActive } from "./state.ts";
|
|
5
5
|
import { resolveActiveRun } from "./run-context.ts";
|
|
6
|
+
import { messaging, tryMessaging } from "./messaging.ts";
|
|
6
7
|
|
|
7
8
|
export const AUTOCOMPLETE_ENTRY = "pi-plans-autocomplete";
|
|
8
9
|
const AUTOCOMPLETE_CONTINUE = "Continue the current planning workflow. Raise the next relevant question with ask_choice; do not stop after an auto-completed answer.";
|
|
@@ -22,12 +23,6 @@ type SessionWithAutoComplete = ExtensionContext["sessionManager"] & {
|
|
|
22
23
|
__piPlansAutoComplete?: AutoCompleteState;
|
|
23
24
|
};
|
|
24
25
|
|
|
25
|
-
let api: ExtensionAPI | null = null;
|
|
26
|
-
|
|
27
|
-
export function setAutoCompleteApi(next: ExtensionAPI | null): void {
|
|
28
|
-
api = next;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
26
|
function sessionState(ctx: ExtensionContext): AutoCompleteState | undefined {
|
|
32
27
|
return (ctx.sessionManager as SessionWithAutoComplete).__piPlansAutoComplete;
|
|
33
28
|
}
|
|
@@ -39,9 +34,17 @@ function activePlanningRun(ctx: ExtensionContext): { runId: string } | null {
|
|
|
39
34
|
return run?.status === "planning" ? { runId: run.run_id } : null;
|
|
40
35
|
}
|
|
41
36
|
|
|
42
|
-
function appendState(runId: string, enabled: boolean, reason?: string): void {
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
function appendState(ctx: ExtensionContext, runId: string, enabled: boolean, reason?: string): void {
|
|
38
|
+
// Best-effort persistence: the in-memory session state is authoritative for
|
|
39
|
+
// the live turn; never let an unwired (tests) or stale (session swap) messaging
|
|
40
|
+
// surface break the ask_choice / handoff flows that call this.
|
|
41
|
+
const m = tryMessaging();
|
|
42
|
+
if (!m) return;
|
|
43
|
+
try {
|
|
44
|
+
m.appendEntry(AUTOCOMPLETE_ENTRY, { runId, enabled, ...(reason ? { reason } : {}) });
|
|
45
|
+
} catch {
|
|
46
|
+
/* session swap in flight; the entry is optional */
|
|
47
|
+
}
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
export function enableAutoComplete(ctx: ExtensionContext): boolean {
|
|
@@ -57,7 +60,7 @@ export function enableAutoComplete(ctx: ExtensionContext): boolean {
|
|
|
57
60
|
autoChoiceCount: 0,
|
|
58
61
|
planWritten: false,
|
|
59
62
|
};
|
|
60
|
-
appendState(run.runId, true);
|
|
63
|
+
appendState(ctx, run.runId, true);
|
|
61
64
|
return true;
|
|
62
65
|
}
|
|
63
66
|
|
|
@@ -67,7 +70,7 @@ export function disableAutoComplete(ctx: ExtensionContext, reason = "disabled"):
|
|
|
67
70
|
const runId = state?.runId ?? run?.runId;
|
|
68
71
|
if (!runId && !state?.enabled) return false;
|
|
69
72
|
if (state) state.enabled = false;
|
|
70
|
-
if (runId) appendState(runId, false, reason);
|
|
73
|
+
if (runId) appendState(ctx, runId, false, reason);
|
|
71
74
|
return true;
|
|
72
75
|
}
|
|
73
76
|
|
|
@@ -110,12 +113,12 @@ export function shouldContinueAutoComplete(ctx: ExtensionContext): boolean {
|
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
export async function continueAutoComplete(ctx: ExtensionContext): Promise<boolean> {
|
|
113
|
-
if (!
|
|
116
|
+
if (!shouldContinueAutoComplete(ctx)) return false;
|
|
114
117
|
const state = sessionState(ctx);
|
|
115
118
|
if (!state) return false;
|
|
116
119
|
state.pendingFollowUp = true;
|
|
117
120
|
try {
|
|
118
|
-
await
|
|
121
|
+
await messaging().sendUserMessage(AUTOCOMPLETE_CONTINUE, { deliverAs: "followUp" });
|
|
119
122
|
return true;
|
|
120
123
|
} catch {
|
|
121
124
|
state.pendingFollowUp = false;
|
|
@@ -123,12 +126,11 @@ export async function continueAutoComplete(ctx: ExtensionContext): Promise<boole
|
|
|
123
126
|
}
|
|
124
127
|
}
|
|
125
128
|
|
|
126
|
-
export function registerAutoCompleteTurnHandlers(
|
|
127
|
-
|
|
128
|
-
pi.on("turn_start", async (_event, ctx) => {
|
|
129
|
+
export function registerAutoCompleteTurnHandlers(ext: ExtensionAPI): void {
|
|
130
|
+
ext.on("turn_start", async (_event, ctx) => {
|
|
129
131
|
resetAutoCompleteTurn(ctx);
|
|
130
132
|
});
|
|
131
|
-
|
|
133
|
+
ext.on("turn_end", async (event, ctx) => {
|
|
132
134
|
const message = event.message as { role?: string } | undefined;
|
|
133
135
|
if (message?.role === "assistant") await continueAutoComplete(ctx);
|
|
134
136
|
});
|
|
@@ -153,7 +153,7 @@ export async function initGraphCommand(args: string, ctx: CommandContext): Promi
|
|
|
153
153
|
"info",
|
|
154
154
|
);
|
|
155
155
|
try {
|
|
156
|
-
const reportPath = generateGraphReport(store, `${paths.gitCommonDir}/
|
|
156
|
+
const reportPath = generateGraphReport(store, `${paths.gitCommonDir}/pi-plans/graph`, {
|
|
157
157
|
files: report.filesScanned,
|
|
158
158
|
functions: report.functionsIndexed,
|
|
159
159
|
ms: report.durationMs,
|
|
@@ -284,7 +284,12 @@ export async function applyGraphCommand(args: string, ctx: CommandContext): Prom
|
|
|
284
284
|
errors > 0 ? "error" : "info",
|
|
285
285
|
);
|
|
286
286
|
const active = resolveActiveRun(ctx.sessionManager, ctx.cwd);
|
|
287
|
-
if (active)
|
|
287
|
+
if (active) {
|
|
288
|
+
// Never demote a verifying run back to executing: the execution-review
|
|
289
|
+
// loop owns the status until it converges (or pauses at the round cap).
|
|
290
|
+
const current = getRun(ctx.cwd, active.run_id)?.status;
|
|
291
|
+
if (current !== "verifying") setRunStatus(ctx.cwd, active.run_id, "executing");
|
|
292
|
+
}
|
|
288
293
|
}
|
|
289
294
|
|
|
290
295
|
export async function graphStatusCommand(_args: string, ctx: CommandContext): Promise<void> {
|
|
@@ -441,7 +446,7 @@ async function runChangedPathSync(
|
|
|
441
446
|
"info",
|
|
442
447
|
);
|
|
443
448
|
try {
|
|
444
|
-
generateGraphReport(store, `${paths.gitCommonDir}/
|
|
449
|
+
generateGraphReport(store, `${paths.gitCommonDir}/pi-plans/graph`, {
|
|
445
450
|
files: report.filesScanned,
|
|
446
451
|
functions: report.functionsIndexed,
|
|
447
452
|
ms: report.durationMs,
|
|
@@ -164,7 +164,7 @@ export function computeCommunities(store: Store): {
|
|
|
164
164
|
return { communities: sortedGroups.length, nodes: order.length, godNodes };
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
/** Render GRAPH_REPORT.md (plan R-005) under `<gitCommonDir>/
|
|
167
|
+
/** Render GRAPH_REPORT.md (plan R-005) under `<gitCommonDir>/pi-plans/graph/`. */
|
|
168
168
|
export function generateGraphReport(store: Store, graphDir: string, baseline?: { files: number; functions: number; ms: number }): string {
|
|
169
169
|
const stats = computeCommunities(store);
|
|
170
170
|
const edgeStats = store.read(() =>
|
package/src/code-graph/paths.ts
CHANGED
|
@@ -34,7 +34,7 @@ export function resolveCanonicalWorktree(workdir: string): WorktreePaths {
|
|
|
34
34
|
}
|
|
35
35
|
const worktreeRoot = path.resolve(toplevel.stdout.trim());
|
|
36
36
|
const gitCommonDir = path.resolve(workdir, common.stdout.trim());
|
|
37
|
-
const stateRoot = path.join(gitCommonDir, "
|
|
37
|
+
const stateRoot = path.join(gitCommonDir, "pi-plans");
|
|
38
38
|
if (!resolveStateRootOrNull(workdir)) {
|
|
39
39
|
throw new PathError(`pi-plans state missing; run a planning init first`);
|
|
40
40
|
}
|
package/src/code-graph/watch.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* pinned to the pi extension hooks — started by /watch-graph (or session_start
|
|
5
5
|
* when the enabled marker is set), stopped by /unwatch-graph, disable-graph,
|
|
6
6
|
* and an idempotent session_shutdown handler. A PID+heartbeat lock under
|
|
7
|
-
* .git/
|
|
7
|
+
* .git/pi-plans/graph/watch keeps a single writer per worktree across pi
|
|
8
8
|
* sessions; per-file single-flight dedupes apply-trigger vs watch events.
|
|
9
9
|
*/
|
|
10
10
|
|
|
@@ -32,7 +32,7 @@ interface ActiveWatcher {
|
|
|
32
32
|
const activeWatchers = new Map<string, ActiveWatcher>();
|
|
33
33
|
|
|
34
34
|
function graphDir(paths: WorktreePaths): string {
|
|
35
|
-
return path.join(paths.gitCommonDir, "
|
|
35
|
+
return path.join(paths.gitCommonDir, "pi-plans", "graph");
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
function lockPath(paths: WorktreePaths): string {
|
package/src/compaction.ts
CHANGED
|
@@ -994,9 +994,9 @@ function phaseContextLines(context?: PiPlansVccPhaseContext): Partial<Record<typ
|
|
|
994
994
|
const outstandingContext: string[] = [];
|
|
995
995
|
if (context.phase === "execution") {
|
|
996
996
|
sessionGoal.push(context.planPath ? `Execute accepted plan ${context.planPath}` : "Execute the accepted pi-plans plan");
|
|
997
|
-
if (context.currentI) outstandingContext.push(`Current
|
|
998
|
-
if (context.remainingVerifierIds?.length) outstandingContext.push(`Remaining
|
|
999
|
-
if (context.implementationIds?.length) outstandingContext.push(`
|
|
997
|
+
if (context.currentI) outstandingContext.push(`Current task: ${context.currentI}`);
|
|
998
|
+
if (context.remainingVerifierIds?.length) outstandingContext.push(`Remaining verification checks: ${context.remainingVerifierIds.slice(0, 12).join(", ")}`);
|
|
999
|
+
if (context.implementationIds?.length) outstandingContext.push(`Plan tasks: ${context.implementationIds.slice(0, 24).join(", ")}`);
|
|
1000
1000
|
} else {
|
|
1001
1001
|
sessionGoal.push(context.runId ? `Continue active planning run ${context.runId}` : "Continue active pi-plans planning");
|
|
1002
1002
|
if (context.planPath) outstandingContext.push(`Latest plan path from session: ${context.planPath}`);
|
package/src/config-command.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
readActive,
|
|
4
|
+
showStateView,
|
|
5
|
+
setRole,
|
|
6
|
+
type PlansConfig,
|
|
7
|
+
utcNow,
|
|
8
|
+
VALID_ROLE_MODES,
|
|
9
|
+
updateConfig,
|
|
10
|
+
type GlobalRoleConfig,
|
|
11
|
+
} from "./state.ts";
|
|
12
|
+
import { DEFAULT_LEVEL_SENTINEL } from "./thinking-levels.ts";
|
|
13
|
+
import { effortItems, runFirstUseFlow, type RolePanelHost } from "./role-panels.ts";
|
|
3
14
|
import { refreshUiLanguage } from "./exec.ts";
|
|
4
15
|
|
|
5
16
|
interface ModelLike {
|
|
@@ -10,12 +21,15 @@ interface ModelLike {
|
|
|
10
21
|
export interface ConfigCommandContext {
|
|
11
22
|
cwd: string;
|
|
12
23
|
hasUI: boolean;
|
|
24
|
+
mode?: string;
|
|
13
25
|
model?: unknown;
|
|
14
26
|
scopedModels?: Array<{ model?: unknown }>;
|
|
15
27
|
modelRegistry?: {
|
|
16
28
|
getAvailable?: () => unknown[];
|
|
29
|
+
find?: (provider: string, id: string) => unknown;
|
|
17
30
|
};
|
|
18
|
-
ui: Pick<ExtensionContext["ui"], "notify" | "select" | "input"
|
|
31
|
+
ui: Pick<ExtensionContext["ui"], "notify" | "select" | "input"> &
|
|
32
|
+
Partial<Pick<ExtensionContext["ui"], "custom">>;
|
|
19
33
|
}
|
|
20
34
|
|
|
21
35
|
type ChoiceResult<T> =
|
|
@@ -142,7 +156,7 @@ async function promptLanguage(ctx: ConfigCommandContext, current: string | null)
|
|
|
142
156
|
|
|
143
157
|
async function promptArtifactRoot(ctx: ConfigCommandContext, current: string): Promise<ChoiceResult<string>> {
|
|
144
158
|
const options: Array<MenuOption<string>> = [{ label: `Keep current (${current})`, value: current }];
|
|
145
|
-
for (const root of ["
|
|
159
|
+
for (const root of ["./.git/pi-plans/plans", "./docs/pi-plans"]) {
|
|
146
160
|
if (root === current) continue;
|
|
147
161
|
options.push({ label: root, value: root });
|
|
148
162
|
}
|
|
@@ -188,7 +202,7 @@ async function promptGraphEnabled(ctx: ConfigCommandContext, current: boolean |
|
|
|
188
202
|
return promptMenu(ctx, "Code graph?", options);
|
|
189
203
|
}
|
|
190
204
|
|
|
191
|
-
async function promptRoleMode(ctx: ConfigCommandContext,
|
|
205
|
+
async function promptRoleMode(ctx: ConfigCommandContext, current: string): Promise<ChoiceResult<string>> {
|
|
192
206
|
if (!VALID_ROLE_MODES.has(current)) {
|
|
193
207
|
current = "delegated-subagent";
|
|
194
208
|
}
|
|
@@ -202,56 +216,115 @@ async function promptRoleMode(ctx: ConfigCommandContext, role: "reviewer" | "cri
|
|
|
202
216
|
{ label: "Keep current-session", value: "current-session" },
|
|
203
217
|
{ label: "Switch to delegated-subagent", value: "delegated-subagent" },
|
|
204
218
|
];
|
|
205
|
-
return promptMenu(ctx,
|
|
219
|
+
return promptMenu(ctx, "Reviewer mode?", options);
|
|
206
220
|
}
|
|
207
221
|
|
|
208
|
-
async function
|
|
222
|
+
async function promptRoleModelEntry(
|
|
209
223
|
ctx: ConfigCommandContext,
|
|
210
|
-
role:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
: "Keep current default (inherit)",
|
|
221
|
-
value: currentSelector,
|
|
222
|
-
},
|
|
224
|
+
role: GlobalRoleConfig,
|
|
225
|
+
): Promise<ChoiceResult<"keep" | "change">> {
|
|
226
|
+
const level = role.thinking_level ?? DEFAULT_LEVEL_SENTINEL;
|
|
227
|
+
const keepLabel =
|
|
228
|
+
role.model_selector !== null && role.confirmed_at !== null
|
|
229
|
+
? `Keep current (${role.model_selector} · ${level})`
|
|
230
|
+
: "Keep current (unconfirmed; first refine will ask)";
|
|
231
|
+
const options: Array<MenuOption<"keep" | "change">> = [
|
|
232
|
+
{ label: keepLabel, value: "keep" },
|
|
233
|
+
{ label: "Choose model & thinking level…", value: "change" },
|
|
223
234
|
];
|
|
224
|
-
|
|
225
|
-
|
|
235
|
+
return promptMenu(ctx, "Reviewer model?", options);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Change flow (Q-3=A): TUI pops the native panels via runFirstUseFlow
|
|
239
|
+
* (which persists to the global config on completion); other UI modes use
|
|
240
|
+
* menus (model list → effort list). Esc anywhere keeps the current role —
|
|
241
|
+
* the wizard continues instead of discarding earlier answers (F-011). */
|
|
242
|
+
async function changeReviewerRole(
|
|
243
|
+
ctx: ConfigCommandContext,
|
|
244
|
+
role: GlobalRoleConfig,
|
|
245
|
+
): Promise<{ role: GlobalRoleConfig; changed: boolean; error?: string }> {
|
|
246
|
+
const host = ctx as unknown as RolePanelHost;
|
|
247
|
+
if (ctx.mode === "tui" && typeof ctx.ui.custom === "function") {
|
|
248
|
+
const outcome = await runFirstUseFlow(host, role.thinking_level);
|
|
249
|
+
if (outcome.status === "confirmed") return { role: outcome.role, changed: true };
|
|
250
|
+
if (outcome.status === "cancelled") {
|
|
251
|
+
ctx.ui.notify("Reviewer change cancelled — keeping the current role.", "warning");
|
|
252
|
+
return { role, changed: false };
|
|
253
|
+
}
|
|
254
|
+
return { role, changed: false, error: "native panels unavailable in this session" };
|
|
226
255
|
}
|
|
256
|
+
// Menus: model list (live model first, then scoped/registry), then effort.
|
|
257
|
+
const currentSelector = role.model_selector;
|
|
258
|
+
const selectors: string[] = [];
|
|
259
|
+
const live = modelSelectorOf(ctx.model);
|
|
260
|
+
if (live) selectors.push(live);
|
|
227
261
|
for (const selector of collectModelSelectors(ctx, currentSelector)) {
|
|
228
|
-
if (selector ===
|
|
229
|
-
|
|
262
|
+
if (selector === live) continue;
|
|
263
|
+
selectors.push(selector);
|
|
230
264
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
265
|
+
const modelLabels = [...selectors.map((selector) => `Use ${selector}`), "Other..."];
|
|
266
|
+
const picked = await ctx.ui.select("Reviewer model?", modelLabels);
|
|
267
|
+
if (picked === undefined) {
|
|
268
|
+
ctx.ui.notify("Reviewer change cancelled — keeping the current role.", "warning");
|
|
269
|
+
return { role, changed: false };
|
|
270
|
+
}
|
|
271
|
+
let selector: string | null = null;
|
|
272
|
+
if (picked === "Other...") {
|
|
273
|
+
const raw = await ctx.ui.input("Reviewer model selector:");
|
|
274
|
+
if (raw === undefined) {
|
|
275
|
+
ctx.ui.notify("Reviewer change cancelled — keeping the current role.", "warning");
|
|
276
|
+
return { role, changed: false };
|
|
277
|
+
}
|
|
278
|
+
selector = parseModelSelector(raw);
|
|
279
|
+
if (selector === null) {
|
|
280
|
+
ctx.ui.notify("Model selector must be an exact provider/model string.", "error");
|
|
281
|
+
return { role, changed: false, error: "invalid model selector" };
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
selector = picked.replace(/^Use /, "");
|
|
285
|
+
}
|
|
286
|
+
// Effort menu: default sentinel + the chosen model's supported levels.
|
|
287
|
+
const found =
|
|
288
|
+
typeof ctx.modelRegistry?.find === "function"
|
|
289
|
+
? ((ctx.modelRegistry.find(selector.split("/")[0]!, selector.split("/")[1]!) as never) ?? null)
|
|
290
|
+
: null;
|
|
291
|
+
const items = found ? effortItems(found, role.thinking_level) : [{ value: DEFAULT_LEVEL_SENTINEL, label: DEFAULT_LEVEL_SENTINEL, description: "no --thinking flag" }];
|
|
292
|
+
const levelLabels = items.map((item) => `${item.label} — ${item.description}`);
|
|
293
|
+
const levelPicked = await ctx.ui.select("Reviewer thinking level? (first row = default: no --thinking flag)", levelLabels);
|
|
294
|
+
const levelValue = levelPicked === undefined ? DEFAULT_LEVEL_SENTINEL : items[levelLabels.indexOf(levelPicked)]?.value ?? DEFAULT_LEVEL_SENTINEL;
|
|
295
|
+
try {
|
|
296
|
+
const applied = setRole(ctx.cwd, {
|
|
297
|
+
role: "reviewer",
|
|
298
|
+
modelSelector: selector,
|
|
299
|
+
thinkingLevel: levelValue,
|
|
300
|
+
confirmed: true,
|
|
301
|
+
});
|
|
302
|
+
return { role: applied.global.reviewer, changed: true };
|
|
303
|
+
} catch (error) {
|
|
304
|
+
return { role, changed: false, error: (error as Error).message };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
interface WizardCurrent {
|
|
309
|
+
config: PlansConfig;
|
|
310
|
+
reviewer: GlobalRoleConfig;
|
|
238
311
|
}
|
|
239
312
|
|
|
240
|
-
function currentConfig(workdir: string):
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
return showConfig(workdir);
|
|
313
|
+
function currentConfig(workdir: string): WizardCurrent {
|
|
314
|
+
const view = showStateView(workdir);
|
|
315
|
+
return { config: view.config, reviewer: view.reviewer };
|
|
244
316
|
}
|
|
245
317
|
|
|
246
|
-
function summarizeConfig(config: PlansConfig): string[] {
|
|
318
|
+
function summarizeConfig(config: PlansConfig, reviewer: GlobalRoleConfig): string[] {
|
|
319
|
+
const level = reviewer.thinking_level ?? DEFAULT_LEVEL_SENTINEL;
|
|
320
|
+
const model = reviewer.model_selector ?? (reviewer.mode === "current-session" ? "(in-session)" : "(unconfirmed)");
|
|
247
321
|
return [
|
|
248
322
|
"pi-plans config updated.",
|
|
249
323
|
`Language: ${config.language.tag ?? "(unset)"}`,
|
|
250
324
|
`Artifact root: ${config.artifact_root}`,
|
|
251
325
|
`Refs root: ${config.refs_root ?? "(unset)"}`,
|
|
252
326
|
`Code graph: ${config.graph_enabled === true ? "enabled" : config.graph_enabled === false ? "disabled" : "unset"}`,
|
|
253
|
-
`Reviewer: ${
|
|
254
|
-
`Criticizer: ${config.criticizer.mode} / ${config.criticizer.model_selector ?? "inherit"}`,
|
|
327
|
+
`Reviewer (global): ${reviewer.mode} / ${model}${reviewer.mode === "current-session" ? "" : ` · ${level}`}`,
|
|
255
328
|
];
|
|
256
329
|
}
|
|
257
330
|
|
|
@@ -264,8 +337,10 @@ export async function configPiPlansCommand(_args: string, ctx: ConfigCommandCont
|
|
|
264
337
|
try {
|
|
265
338
|
const workdir = ctx.cwd;
|
|
266
339
|
const current = currentConfig(workdir);
|
|
340
|
+
let reviewer = current.reviewer;
|
|
341
|
+
const workspace = current.config;
|
|
267
342
|
|
|
268
|
-
const language = await promptLanguage(ctx,
|
|
343
|
+
const language = await promptLanguage(ctx, workspace.language.tag);
|
|
269
344
|
if (language.cancelled) {
|
|
270
345
|
if (language.reason === "user") {
|
|
271
346
|
ctx.ui.notify("Configuration wizard cancelled. No changes were written.", "warning");
|
|
@@ -273,7 +348,7 @@ export async function configPiPlansCommand(_args: string, ctx: ConfigCommandCont
|
|
|
273
348
|
return;
|
|
274
349
|
}
|
|
275
350
|
|
|
276
|
-
const artifactRoot = await promptArtifactRoot(ctx,
|
|
351
|
+
const artifactRoot = await promptArtifactRoot(ctx, workspace.artifact_root);
|
|
277
352
|
if (artifactRoot.cancelled) {
|
|
278
353
|
if (artifactRoot.reason === "user") {
|
|
279
354
|
ctx.ui.notify("Configuration wizard cancelled. No changes were written.", "warning");
|
|
@@ -281,7 +356,7 @@ export async function configPiPlansCommand(_args: string, ctx: ConfigCommandCont
|
|
|
281
356
|
return;
|
|
282
357
|
}
|
|
283
358
|
|
|
284
|
-
const refsRoot = await promptRefsRoot(ctx,
|
|
359
|
+
const refsRoot = await promptRefsRoot(ctx, workspace.refs_root);
|
|
285
360
|
if (refsRoot.cancelled) {
|
|
286
361
|
if (refsRoot.reason === "user") {
|
|
287
362
|
ctx.ui.notify("Configuration wizard cancelled. No changes were written.", "warning");
|
|
@@ -289,7 +364,7 @@ export async function configPiPlansCommand(_args: string, ctx: ConfigCommandCont
|
|
|
289
364
|
return;
|
|
290
365
|
}
|
|
291
366
|
|
|
292
|
-
const graphEnabled = await promptGraphEnabled(ctx,
|
|
367
|
+
const graphEnabled = await promptGraphEnabled(ctx, workspace.graph_enabled);
|
|
293
368
|
if (graphEnabled.cancelled) {
|
|
294
369
|
if (graphEnabled.reason === "user") {
|
|
295
370
|
ctx.ui.notify("Configuration wizard cancelled. No changes were written.", "warning");
|
|
@@ -297,36 +372,40 @@ export async function configPiPlansCommand(_args: string, ctx: ConfigCommandCont
|
|
|
297
372
|
return;
|
|
298
373
|
}
|
|
299
374
|
|
|
300
|
-
const reviewerMode = await promptRoleMode(ctx,
|
|
375
|
+
const reviewerMode = await promptRoleMode(ctx, reviewer.mode);
|
|
301
376
|
if (reviewerMode.cancelled) {
|
|
302
377
|
if (reviewerMode.reason === "user") {
|
|
303
378
|
ctx.ui.notify("Configuration wizard cancelled. No changes were written.", "warning");
|
|
304
379
|
}
|
|
305
380
|
return;
|
|
306
381
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
382
|
+
if (reviewerMode.value !== reviewer.mode) {
|
|
383
|
+
// Mode lives in the global role: persist the switch immediately;
|
|
384
|
+
// the model/thinking level stays untouched.
|
|
385
|
+
try {
|
|
386
|
+
const applied = setRole(workdir, { role: "reviewer", mode: reviewerMode.value });
|
|
387
|
+
reviewer = applied.global.reviewer;
|
|
388
|
+
} catch (error) {
|
|
389
|
+
ctx.ui.notify(`Failed to switch the reviewer mode: ${(error as Error).message}`, "error");
|
|
312
390
|
}
|
|
313
|
-
return;
|
|
314
391
|
}
|
|
315
392
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
393
|
+
// Q-3=A: current-session never uses a spawned model — skip the model
|
|
394
|
+
// step entirely (and never clear an existing selector).
|
|
395
|
+
let reviewerError: string | undefined;
|
|
396
|
+
if (reviewer.mode === "delegated-subagent") {
|
|
397
|
+
const entry = await promptRoleModelEntry(ctx, reviewer);
|
|
398
|
+
if (entry.cancelled) {
|
|
399
|
+
if (entry.reason === "user") {
|
|
400
|
+
ctx.ui.notify("Configuration wizard cancelled. No changes were written.", "warning");
|
|
401
|
+
}
|
|
402
|
+
return;
|
|
320
403
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (criticizerModel.cancelled) {
|
|
326
|
-
if (criticizerModel.reason === "user") {
|
|
327
|
-
ctx.ui.notify("Configuration wizard cancelled. No changes were written.", "warning");
|
|
404
|
+
if (entry.value === "change") {
|
|
405
|
+
const changed = await changeReviewerRole(ctx, reviewer);
|
|
406
|
+
reviewer = changed.role;
|
|
407
|
+
reviewerError = changed.error;
|
|
328
408
|
}
|
|
329
|
-
return;
|
|
330
409
|
}
|
|
331
410
|
|
|
332
411
|
const updated = updateConfig(workdir, (config) => {
|
|
@@ -340,18 +419,9 @@ export async function configPiPlansCommand(_args: string, ctx: ConfigCommandCont
|
|
|
340
419
|
config.refs_root_updated_at = now;
|
|
341
420
|
config.graph_enabled = graphEnabled.value;
|
|
342
421
|
config.graph_enabled_updated_at = now;
|
|
343
|
-
config.
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
model_selector: reviewerModel.value,
|
|
347
|
-
confirmed_at: now,
|
|
348
|
-
};
|
|
349
|
-
config.criticizer = {
|
|
350
|
-
...config.criticizer,
|
|
351
|
-
mode: criticizerMode.value,
|
|
352
|
-
model_selector: criticizerModel.value,
|
|
353
|
-
confirmed_at: now,
|
|
354
|
-
};
|
|
422
|
+
// The reviewer role lives in the GLOBAL config since v0.7.0 — the
|
|
423
|
+
// wizard only strips a legacy workspace key, never writes one.
|
|
424
|
+
delete config.reviewer;
|
|
355
425
|
return config;
|
|
356
426
|
});
|
|
357
427
|
|
|
@@ -362,7 +432,10 @@ export async function configPiPlansCommand(_args: string, ctx: ConfigCommandCont
|
|
|
362
432
|
// Display-only consumer (F-006): deliberately keeps the shared active
|
|
363
433
|
// pointer — the wizard summarizes repo state, not session attribution.
|
|
364
434
|
const active = readActive(workdir);
|
|
365
|
-
const lines = summarizeConfig(updated.config);
|
|
435
|
+
const lines = summarizeConfig(updated.config, reviewer);
|
|
436
|
+
if (reviewerError) {
|
|
437
|
+
lines.push(`Reviewer change failed (${reviewerError}); the reviewer role is unchanged. Workspace settings above were still written.`);
|
|
438
|
+
}
|
|
366
439
|
if (active) {
|
|
367
440
|
lines.push(`Active run left unchanged: ${active.run_id}`);
|
|
368
441
|
}
|