pi-plans 0.5.7 → 0.7.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/CONTRIBUTING.md +126 -0
- package/README.md +49 -39
- package/agents/ref-analyst.md +7 -4
- package/agents/reviewer.md +12 -3
- package/index.ts +74 -40
- package/package.json +2 -1
- package/references/pi-planning-workflow.md +45 -58
- package/references/plan-artifact-template.md +71 -60
- package/references/state-and-config.md +63 -47
- 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 +22 -10
- package/skills/debug-and-plan/SKILL.md +4 -4
- package/skills/plan-big/SKILL.md +5 -5
- package/skills/plan-normal/SKILL.md +5 -5
- package/skills/plan-small/SKILL.md +5 -5
- package/skills/plan-with-refs/SKILL.md +8 -8
- package/skills/planning/SKILL.md +1 -1
- package/src/ask-form.ts +4 -4
- package/src/auditor.ts +126 -0
- package/src/auto-approve.ts +1 -1
- package/src/autocomplete.ts +19 -17
- package/src/code-graph/commands.ts +2 -2
- 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 +154 -76
- package/src/dashboard.ts +257 -0
- package/src/exec.ts +709 -705
- package/src/global-state.ts +304 -0
- package/src/guard.ts +16 -3
- package/src/messaging.ts +44 -0
- package/src/plan.ts +421 -112
- package/src/query-hook.ts +4 -4
- package/src/refine-prompts.ts +14 -72
- package/src/refine-ui-helpers.ts +24 -5
- package/src/refine-ui-state.ts +1 -1
- package/src/refine-ui.ts +1 -1
- package/src/resume-command.ts +40 -130
- package/src/resume.ts +15 -17
- package/src/role-panels.ts +542 -0
- package/src/run-context.ts +5 -4
- package/src/run-picker.ts +98 -0
- package/src/state.ts +380 -77
- package/src/subagent.ts +32 -1
- package/src/task-tool.ts +100 -0
- package/src/tasks.ts +189 -0
- package/src/thinking-levels.ts +67 -0
- package/src/ui-language.ts +3 -54
- package/src/workflow-state.ts +78 -57
- package/tests/analyze-refs.test.ts +35 -18
- package/tests/ask-choice-pros-cons.test.ts +147 -0
- 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 +111 -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 +268 -0
- package/tests/exec-lifecycle.test.ts +181 -115
- package/tests/exec-panel-lifecycle.test.ts +106 -251
- package/tests/exec.test.ts +617 -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 +184 -0
- 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 +37 -22
- package/tests/resume.test.ts +43 -88
- 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/state.test.ts +151 -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/thinking-levels.test.ts +77 -0
- package/tests/ui-language.test.ts +2 -17
- package/tests/workflow-state.test.ts +17 -99
- package/tools/analyze-refs.ts +67 -32
- package/tools/ask-choice.ts +19 -49
- package/tools/code-graph.ts +2 -2
- package/tools/execute-plan.ts +63 -33
- package/tools/graph-aware-file-tools.ts +6 -4
- package/tools/plans.ts +40 -66
- package/tools/refine.ts +101 -164
- package/agents/criticizer.md +0 -18
- 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
|
@@ -63,7 +63,7 @@ test("runIndex indexes files, writes rows, and screening excludes full_code", as
|
|
|
63
63
|
};
|
|
64
64
|
const sqlite = await import("node:sqlite");
|
|
65
65
|
const commonDir = git(worktreeRoot, ["rev-parse", "--git-common-dir"]);
|
|
66
|
-
const dbDir = path.join(path.resolve(worktreeRoot, commonDir), "
|
|
66
|
+
const dbDir = path.join(path.resolve(worktreeRoot, commonDir), "pi-plans");
|
|
67
67
|
fs.mkdirSync(dbDir, { recursive: true });
|
|
68
68
|
const dbPath = path.join(dbDir, "code_graph.db");
|
|
69
69
|
try { fs.unlinkSync(dbPath); } catch { /* ignore */ }
|
|
@@ -151,7 +151,7 @@ test("runIndex with opts.paths indexes only hit paths and purges missing ones",
|
|
|
151
151
|
tsx: makeBackend("tsx", ParserCtor, runtime.runtime.parser.tsx),
|
|
152
152
|
python: new PythonBackend(ParserCtor, runtime.runtime.parser.python),
|
|
153
153
|
};
|
|
154
|
-
const stateRoot = path.join(worktreeRoot, ".git", "
|
|
154
|
+
const stateRoot = path.join(worktreeRoot, ".git", "pi-plans");
|
|
155
155
|
fs.mkdirSync(stateRoot, { recursive: true });
|
|
156
156
|
const store = new Store({ dbPath: path.join(stateRoot, "code_graph.db"), worktreeRoot, gitCommonDir: path.join(worktreeRoot, ".git") }, runtime.runtime.sqlite);
|
|
157
157
|
t.after(() => store.close());
|
|
@@ -57,7 +57,7 @@ test("closed loop: init → mutation → apply → drift → final-commit → cl
|
|
|
57
57
|
const { root, cleanup } = initRepo();
|
|
58
58
|
t.after(cleanup);
|
|
59
59
|
|
|
60
|
-
const stateRoot = path.join(root, ".git", "
|
|
60
|
+
const stateRoot = path.join(root, ".git", "pi-plans");
|
|
61
61
|
fs.mkdirSync(stateRoot, { recursive: true });
|
|
62
62
|
const dbPath = path.join(stateRoot, "code_graph.db");
|
|
63
63
|
const store = new Store({ dbPath, worktreeRoot: root, gitCommonDir: path.join(root, ".git") }, runtime.runtime.sqlite);
|
|
@@ -51,7 +51,7 @@ test("updateFunction propagates offsets, rebuilds source exactly, and applies by
|
|
|
51
51
|
if (!runtime.status.parserAvailable || !runtime.status.sqliteAvailable) return;
|
|
52
52
|
const { root, cleanup } = initRepo();
|
|
53
53
|
t.after(cleanup);
|
|
54
|
-
const stateRoot = path.join(root, ".git", "
|
|
54
|
+
const stateRoot = path.join(root, ".git", "pi-plans");
|
|
55
55
|
fs.mkdirSync(stateRoot, { recursive: true });
|
|
56
56
|
const store = new Store({ dbPath: path.join(stateRoot, "g.db"), worktreeRoot: root, gitCommonDir: path.join(root, ".git") }, runtime.runtime.sqlite);
|
|
57
57
|
t.after(() => store.close());
|
|
@@ -59,7 +59,7 @@ test("indexer rolls back the whole file batch when an emit step throws", async (
|
|
|
59
59
|
};
|
|
60
60
|
const sqlite = await import("node:sqlite");
|
|
61
61
|
const commonDir = git(worktreeRoot, ["rev-parse", "--git-common-dir"]);
|
|
62
|
-
const dbDir = path.join(path.resolve(worktreeRoot, commonDir), "
|
|
62
|
+
const dbDir = path.join(path.resolve(worktreeRoot, commonDir), "pi-plans");
|
|
63
63
|
fs.mkdirSync(dbDir, { recursive: true });
|
|
64
64
|
const dbPath = path.join(dbDir, "code_graph.db");
|
|
65
65
|
try { fs.unlinkSync(dbPath); } catch { /* ignore */ }
|
|
@@ -362,8 +362,8 @@ describe("watch lifecycle (VC-006)", () => {
|
|
|
362
362
|
execSync('git config user.email t@t && git config user.name t', { cwd: root, shell: "/bin/bash" });
|
|
363
363
|
const { parsers, sqlite } = await makeParsers();
|
|
364
364
|
fs.writeFileSync(path.join(root, "a.ts"), "export function alpha() { return 1; }\n");
|
|
365
|
-
// The watcher always opens the CANONICAL db under .git/
|
|
366
|
-
const dbPath = path.join(root, ".git", "
|
|
365
|
+
// The watcher always opens the CANONICAL db under .git/pi-plans/.
|
|
366
|
+
const dbPath = path.join(root, ".git", "pi-plans", "code_graph.db");
|
|
367
367
|
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
368
368
|
const store = new Store({ dbPath, worktreeRoot: root, gitCommonDir: path.join(root, ".git") }, sqlite);
|
|
369
369
|
await runIndex({ store, worktreeRoot: root, parsers });
|
package/tests/compaction.test.ts
CHANGED
|
@@ -245,7 +245,7 @@ describe("pi-vcc compaction", () => {
|
|
|
245
245
|
assert.match(result.compaction.summary, /Modified: src\/compaction\.ts/);
|
|
246
246
|
assert.match(result.compaction.summary, /Read: src\/exec\.ts/);
|
|
247
247
|
assert.match(result.compaction.summary, /\[Outstanding Context\]/);
|
|
248
|
-
assert.match(result.compaction.summary, /Current
|
|
248
|
+
assert.match(result.compaction.summary, /Current task: I-002/);
|
|
249
249
|
assert.match(result.compaction.summary, /Previous compact summary: Legacy Summary Earlier compact facts\./);
|
|
250
250
|
assert.match(result.compaction.summary, /\[User Preferences\]/);
|
|
251
251
|
assert.match(result.compaction.summary, /Always keep ASCII output/);
|
|
@@ -3,7 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import * as url from "node:url";
|
|
6
|
-
import { after, before, describe, it } from "node:test";
|
|
6
|
+
import { after, before, beforeEach, describe, it } from "node:test";
|
|
7
7
|
|
|
8
8
|
import { configPiPlansCommand } from "../src/config-command.ts";
|
|
9
9
|
import { startRun } from "../src/state.ts";
|
|
@@ -25,13 +25,25 @@ interface HarnessOptions {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
let root: string;
|
|
28
|
+
let globalDir: string;
|
|
29
|
+
let previousGlobalDir: string | undefined;
|
|
28
30
|
|
|
29
31
|
before(() => {
|
|
30
32
|
root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-plans-config-command-"));
|
|
33
|
+
previousGlobalDir = process.env.PI_PLANS_GLOBAL_DIR;
|
|
34
|
+
globalDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-plans-global-config-command-"));
|
|
35
|
+
process.env.PI_PLANS_GLOBAL_DIR = globalDir;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
fs.rmSync(path.join(globalDir, "config.json"), { force: true });
|
|
31
40
|
});
|
|
32
41
|
|
|
33
42
|
after(() => {
|
|
43
|
+
if (previousGlobalDir === undefined) delete process.env.PI_PLANS_GLOBAL_DIR;
|
|
44
|
+
else process.env.PI_PLANS_GLOBAL_DIR = previousGlobalDir;
|
|
34
45
|
fs.rmSync(root, { recursive: true, force: true });
|
|
46
|
+
fs.rmSync(globalDir, { recursive: true, force: true });
|
|
35
47
|
});
|
|
36
48
|
|
|
37
49
|
function workdir(name: string): string {
|
|
@@ -41,11 +53,15 @@ function workdir(name: string): string {
|
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
function configPath(workdirPath: string): string {
|
|
44
|
-
return path.join(workdirPath, ".git", "
|
|
56
|
+
return path.join(workdirPath, ".git", "pi-plans", "config.json");
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
function runPath(workdirPath: string, runId: string): string {
|
|
48
|
-
return path.join(workdirPath, ".git", "
|
|
60
|
+
return path.join(workdirPath, ".git", "pi-plans", "runs", runId, "run.json");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readGlobal(): any {
|
|
64
|
+
return JSON.parse(fs.readFileSync(path.join(globalDir, "config.json"), "utf8"));
|
|
49
65
|
}
|
|
50
66
|
|
|
51
67
|
function readJson(filePath: string): any {
|
|
@@ -61,6 +77,7 @@ function makeContext(workdirPath: string, options: HarnessOptions) {
|
|
|
61
77
|
const ctx = {
|
|
62
78
|
cwd: workdirPath,
|
|
63
79
|
hasUI: true,
|
|
80
|
+
mode: "rpc",
|
|
64
81
|
model: options.model ?? null,
|
|
65
82
|
scopedModels: options.scopedModels ?? [],
|
|
66
83
|
modelRegistry: {
|
|
@@ -83,6 +100,15 @@ function makeContext(workdirPath: string, options: HarnessOptions) {
|
|
|
83
100
|
return { ctx, recorded };
|
|
84
101
|
}
|
|
85
102
|
|
|
103
|
+
/** Default answers for the four workspace menus (language/artifact/refs/graph). */
|
|
104
|
+
function workspaceDefaults(question: string, labels: string[]): string | undefined {
|
|
105
|
+
if (question === "Language?") return labels[0];
|
|
106
|
+
if (question === "Artifact root?") return labels[0];
|
|
107
|
+
if (question === "Refs root (plan-with-refs downloads)?") return labels[0];
|
|
108
|
+
if (question === "Code graph?") return labels[0];
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
86
112
|
describe("config-pi-plans command", () => {
|
|
87
113
|
it("registers the slash command in index.ts and delegates to the helper", () => {
|
|
88
114
|
const source = fs.readFileSync(path.join(ROOT, "index.ts"), "utf8");
|
|
@@ -91,25 +117,14 @@ describe("config-pi-plans command", () => {
|
|
|
91
117
|
assert.match(source, /await configPiPlansCommand\(args, ctx\);/);
|
|
92
118
|
});
|
|
93
119
|
|
|
94
|
-
it("writes the
|
|
120
|
+
it("writes the workspace settings and switches the reviewer mode globally; current-session skips the model step (Q-3)", async () => {
|
|
95
121
|
const dir = workdir("full-wizard");
|
|
96
|
-
const { ctx } = makeContext(dir, {
|
|
122
|
+
const { ctx, recorded } = makeContext(dir, {
|
|
97
123
|
model: { provider: "live", id: "pro" },
|
|
98
124
|
availableModels: [{ provider: "registry", id: "critic" }],
|
|
99
125
|
select: (question, labels) => {
|
|
100
|
-
if (question === "Language?") return labels.find((label) => label.includes("en"));
|
|
101
|
-
if (question === "Artifact root?") return labels.find((label) => label.includes("./.git/pi_plans/plans"));
|
|
102
|
-
if (question === "Refs root (plan-with-refs downloads)?") return labels.find((label) => label.includes(".git/pi-plans/refs"));
|
|
103
|
-
if (question === "Code graph?") return labels.find((label) => label.includes("Disable code graph"));
|
|
104
126
|
if (question === "Reviewer mode?") return labels.find((label) => label.includes("Switch to current-session"));
|
|
105
|
-
|
|
106
|
-
if (question === "Criticizer mode?") return labels.find((label) => label.includes("Keep delegated-subagent"));
|
|
107
|
-
if (question === "Criticizer model?") return labels.find((label) => label.includes("Other..."));
|
|
108
|
-
return undefined;
|
|
109
|
-
},
|
|
110
|
-
input: (prompt) => {
|
|
111
|
-
if (prompt === "Criticizer model selector:") return "custom/critic-model";
|
|
112
|
-
return undefined;
|
|
127
|
+
return workspaceDefaults(question, labels);
|
|
113
128
|
},
|
|
114
129
|
});
|
|
115
130
|
|
|
@@ -117,24 +132,44 @@ describe("config-pi-plans command", () => {
|
|
|
117
132
|
|
|
118
133
|
assert.ok(fs.existsSync(configPath(dir)));
|
|
119
134
|
const config = readJson(configPath(dir));
|
|
120
|
-
assert.equal(config.language.tag, "
|
|
121
|
-
assert.equal(config.
|
|
122
|
-
assert.equal(config.artifact_root, "./.git/pi_plans/plans");
|
|
123
|
-
assert.equal(config.artifact_root_source, "user");
|
|
135
|
+
assert.equal(config.language.tag, "zh-Hans", "labels[0] on the language menu");
|
|
136
|
+
assert.equal(config.artifact_root, "./.git/pi-plans/plans");
|
|
124
137
|
assert.equal(config.refs_root, ".git/pi-plans/refs");
|
|
125
|
-
assert.equal(config.
|
|
126
|
-
assert.
|
|
127
|
-
assert.equal(config
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
assert.equal(
|
|
131
|
-
|
|
132
|
-
assert.
|
|
133
|
-
|
|
138
|
+
assert.equal(config.graph_enabled, true, "labels[0] on the graph menu enables");
|
|
139
|
+
assert.equal("reviewer" in config, false, "the reviewer role never lands in the workspace config");
|
|
140
|
+
assert.equal("criticizer" in config, false);
|
|
141
|
+
// The mode switch went to the GLOBAL config.
|
|
142
|
+
const globalRole = readGlobal().reviewer;
|
|
143
|
+
assert.equal(globalRole.mode, "current-session");
|
|
144
|
+
// Q-3: no reviewer-model prompt in current-session mode.
|
|
145
|
+
assert.equal(recorded.selects.some((entry) => entry.question === "Reviewer model?"), false);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("shows a keep/change entry menu when the reviewer is unconfirmed", async () => {
|
|
149
|
+
const dir = workdir("entry-menu");
|
|
150
|
+
const { ctx, recorded } = makeContext(dir, {
|
|
151
|
+
select: (question, labels) => {
|
|
152
|
+
if (question === "Reviewer mode?") return labels[0];
|
|
153
|
+
if (question === "Reviewer model?") return labels[0];
|
|
154
|
+
return workspaceDefaults(question, labels);
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await configPiPlansCommand("", ctx);
|
|
159
|
+
|
|
160
|
+
const entry = recorded.selects.find((entry) => entry.question === "Reviewer model?");
|
|
161
|
+
assert.ok(entry);
|
|
162
|
+
assert.deepEqual(entry?.labels, [
|
|
163
|
+
"1. Keep current (unconfirmed; first refine will ask)",
|
|
164
|
+
"2. Choose model & thinking level…",
|
|
165
|
+
]);
|
|
166
|
+
// Keeping the unconfirmed role writes nothing to the global config.
|
|
167
|
+
assert.equal(fs.existsSync(path.join(globalDir, "config.json")), false);
|
|
168
|
+
assert.equal(fs.existsSync(configPath(dir)), true);
|
|
134
169
|
});
|
|
135
170
|
|
|
136
|
-
it("
|
|
137
|
-
const dir = workdir("
|
|
171
|
+
it("change flow via menus persists model + level to the global config", async () => {
|
|
172
|
+
const dir = workdir("change-menus");
|
|
138
173
|
const { ctx, recorded } = makeContext(dir, {
|
|
139
174
|
model: { provider: "live", id: "pro" },
|
|
140
175
|
scopedModels: [{ model: { provider: "scoped", id: "model-a" } }],
|
|
@@ -143,51 +178,56 @@ describe("config-pi-plans command", () => {
|
|
|
143
178
|
{ provider: "registry", id: "model-b" },
|
|
144
179
|
],
|
|
145
180
|
select: (question, labels) => {
|
|
146
|
-
if (question === "Language?") return labels[0];
|
|
147
|
-
if (question === "Artifact root?") return labels[0];
|
|
148
|
-
if (question === "Refs root (plan-with-refs downloads)?") return labels[0];
|
|
149
|
-
if (question === "Code graph?") return labels[0];
|
|
150
181
|
if (question === "Reviewer mode?") return labels[0];
|
|
151
|
-
if (question === "Reviewer model?")
|
|
152
|
-
|
|
153
|
-
|
|
182
|
+
if (question === "Reviewer model?") {
|
|
183
|
+
const modelMenus = recorded.selects.filter((e) => e.question === "Reviewer model?");
|
|
184
|
+
if (modelMenus.length === 1) return labels.find((label) => label.includes("Choose model & thinking level"));
|
|
185
|
+
return labels.find((label) => label.includes("Use registry/model-b"));
|
|
186
|
+
}
|
|
187
|
+
if (question.startsWith("Reviewer thinking level?")) return labels[0];
|
|
188
|
+
return workspaceDefaults(question, labels);
|
|
154
189
|
},
|
|
155
190
|
});
|
|
156
191
|
|
|
157
192
|
await configPiPlansCommand("", ctx);
|
|
158
193
|
|
|
159
|
-
const
|
|
160
|
-
assert.
|
|
161
|
-
assert.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
"5. Other...",
|
|
167
|
-
]);
|
|
168
|
-
assert.equal(fs.existsSync(configPath(dir)), false);
|
|
169
|
-
assert.equal(recorded.notifies.at(-1)?.severity, "warning");
|
|
194
|
+
const globalRole = readGlobal().reviewer;
|
|
195
|
+
assert.equal(globalRole.mode, "delegated-subagent");
|
|
196
|
+
assert.equal(globalRole.model_selector, "registry/model-b");
|
|
197
|
+
assert.equal(globalRole.thinking_level, null, "first row = default → null");
|
|
198
|
+
assert.ok(globalRole.confirmed_at);
|
|
199
|
+
// The workspace config stays reviewer-free.
|
|
200
|
+
assert.equal("reviewer" in readJson(configPath(dir)), false);
|
|
170
201
|
});
|
|
171
202
|
|
|
172
|
-
it("
|
|
203
|
+
it("an invalid manual selector keeps the current role and still writes workspace settings (F-011)", async () => {
|
|
173
204
|
const dir = workdir("invalid-model");
|
|
174
205
|
const { ctx, recorded } = makeContext(dir, {
|
|
175
206
|
select: (question, labels) => {
|
|
176
|
-
if (question === "Language?") return labels[0];
|
|
177
|
-
if (question === "Artifact root?") return labels[0];
|
|
178
|
-
if (question === "Refs root (plan-with-refs downloads)?") return labels[0];
|
|
179
|
-
if (question === "Code graph?") return labels[0];
|
|
180
207
|
if (question === "Reviewer mode?") return labels[0];
|
|
181
|
-
if (question === "Reviewer model?")
|
|
182
|
-
|
|
208
|
+
if (question === "Reviewer model?") {
|
|
209
|
+
const modelMenus = recorded.selects.filter((e) => e.question === "Reviewer model?");
|
|
210
|
+
if (modelMenus.length === 1) return labels.find((label) => label.includes("Choose model & thinking level"));
|
|
211
|
+
return labels.find((label) => label.includes("Other..."));
|
|
212
|
+
}
|
|
213
|
+
return workspaceDefaults(question, labels);
|
|
183
214
|
},
|
|
184
215
|
input: () => "bad selector",
|
|
185
216
|
});
|
|
186
217
|
|
|
187
218
|
await configPiPlansCommand("", ctx);
|
|
188
219
|
|
|
189
|
-
assert.equal(fs.existsSync(
|
|
190
|
-
assert.equal(
|
|
220
|
+
assert.equal(fs.existsSync(path.join(globalDir, "config.json")), false, "nothing persisted for the reviewer");
|
|
221
|
+
assert.equal(fs.existsSync(configPath(dir)), true, "workspace settings still written");
|
|
222
|
+
assert.equal(
|
|
223
|
+
recorded.notifies.some((entry) => entry.message.includes("Model selector must be an exact provider/model string.")),
|
|
224
|
+
true,
|
|
225
|
+
);
|
|
226
|
+
assert.equal(
|
|
227
|
+
recorded.notifies.some((entry) => entry.message.includes("Reviewer change failed")),
|
|
228
|
+
true,
|
|
229
|
+
"the summary reports the failed reviewer change",
|
|
230
|
+
);
|
|
191
231
|
});
|
|
192
232
|
|
|
193
233
|
it("does not write when the wizard is cancelled", async () => {
|
|
@@ -204,49 +244,15 @@ describe("config-pi-plans command", () => {
|
|
|
204
244
|
assert.equal(fs.existsSync(configPath(dir)), false);
|
|
205
245
|
});
|
|
206
246
|
|
|
207
|
-
it("renders a two-option inherit menu when no model sources exist", async () => {
|
|
208
|
-
const dir = workdir("empty-model-menu");
|
|
209
|
-
const { ctx, recorded } = makeContext(dir, {
|
|
210
|
-
model: null,
|
|
211
|
-
scopedModels: [],
|
|
212
|
-
availableModels: [],
|
|
213
|
-
select: (question, labels) => {
|
|
214
|
-
if (question === "Language?") return labels[0];
|
|
215
|
-
if (question === "Artifact root?") return labels[0];
|
|
216
|
-
if (question === "Refs root (plan-with-refs downloads)?") return labels[0];
|
|
217
|
-
if (question === "Code graph?") return labels[0];
|
|
218
|
-
if (question === "Reviewer mode?") return labels[0];
|
|
219
|
-
if (question === "Reviewer model?") return labels[0];
|
|
220
|
-
return undefined;
|
|
221
|
-
},
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
await configPiPlansCommand("", ctx);
|
|
225
|
-
|
|
226
|
-
const reviewerModelPrompt = recorded.selects.find((entry) => entry.question === "Reviewer model?");
|
|
227
|
-
assert.ok(reviewerModelPrompt);
|
|
228
|
-
assert.deepEqual(reviewerModelPrompt?.labels, [
|
|
229
|
-
"1. Keep current default (inherit)",
|
|
230
|
-
"2. Other...",
|
|
231
|
-
]);
|
|
232
|
-
assert.equal(fs.existsSync(configPath(dir)), false);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
247
|
it("keeps the active run snapshot unchanged", async () => {
|
|
236
248
|
const dir = workdir("active-run");
|
|
237
249
|
const { run } = startRun(dir, { topic: "config", skill: "plan-small", requestText: "x" });
|
|
238
250
|
const before = readJson(runPath(dir, run.run_id));
|
|
239
251
|
const { ctx } = makeContext(dir, {
|
|
240
252
|
select: (question, labels) => {
|
|
241
|
-
if (question === "Language?") return labels.find((label) => label.includes("en"));
|
|
242
|
-
if (question === "Artifact root?") return labels.find((label) => label.includes("./.git/pi_plans/plans"));
|
|
243
|
-
if (question === "Refs root (plan-with-refs downloads)?") return labels.find((label) => label.includes(".git/pi-plans/refs"));
|
|
244
|
-
if (question === "Code graph?") return labels.find((label) => label.includes("Enable code graph"));
|
|
245
253
|
if (question === "Reviewer mode?") return labels.find((label) => label.includes("Keep delegated-subagent"));
|
|
246
254
|
if (question === "Reviewer model?") return labels[0];
|
|
247
|
-
|
|
248
|
-
if (question === "Criticizer model?") return labels[0];
|
|
249
|
-
return undefined;
|
|
255
|
+
return workspaceDefaults(question, labels);
|
|
250
256
|
},
|
|
251
257
|
});
|
|
252
258
|
|
|
@@ -255,10 +261,7 @@ describe("config-pi-plans command", () => {
|
|
|
255
261
|
const after = readJson(runPath(dir, run.run_id));
|
|
256
262
|
assert.deepEqual(after, before);
|
|
257
263
|
const config = readJson(configPath(dir));
|
|
258
|
-
assert.equal(config
|
|
259
|
-
assert.equal(config.artifact_root, "./.git/pi_plans/plans");
|
|
260
|
-
assert.equal(config.reviewer.model_selector, null);
|
|
261
|
-
assert.ok(config.reviewer.confirmed_at);
|
|
264
|
+
assert.equal("reviewer" in config, false);
|
|
262
265
|
});
|
|
263
266
|
|
|
264
267
|
it("with an active run, a successful save never notifies an error (impl review F-001)", async () => {
|
|
@@ -266,14 +269,14 @@ describe("config-pi-plans command", () => {
|
|
|
266
269
|
startRun(dir, { topic: "config-guard", skill: "plan-small", requestText: "x" });
|
|
267
270
|
const { ctx, recorded } = makeContext(dir, {
|
|
268
271
|
select: (question, labels) => {
|
|
269
|
-
if (question === "Language?") return labels.find((label) => label.includes("
|
|
272
|
+
if (question === "Language?") return labels.find((label) => label.includes("zh-Hans")) ?? labels[0];
|
|
270
273
|
return labels[0];
|
|
271
274
|
},
|
|
272
275
|
});
|
|
273
276
|
|
|
274
277
|
await configPiPlansCommand("", ctx);
|
|
275
278
|
|
|
276
|
-
assert.equal(
|
|
279
|
+
assert.equal(fs.existsSync(configPath(dir)), true, "the wizard must keep saving");
|
|
277
280
|
assert.equal(
|
|
278
281
|
recorded.notifies.find((entry) => entry.severity === "error"),
|
|
279
282
|
undefined,
|