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.
Files changed (112) hide show
  1. package/CONTRIBUTING.md +126 -0
  2. package/README.md +49 -39
  3. package/agents/ref-analyst.md +7 -4
  4. package/agents/reviewer.md +12 -3
  5. package/index.ts +74 -40
  6. package/package.json +2 -1
  7. package/references/pi-planning-workflow.md +45 -58
  8. package/references/plan-artifact-template.md +71 -60
  9. package/references/state-and-config.md +63 -47
  10. package/scripts/bench/pi-adapter/pi_plans_bench.py +33 -22
  11. package/scripts/bench/pi-adapter/rpc_driver.mjs +4 -4
  12. package/scripts/run-tests.ts +12 -1
  13. package/scripts/validate.ts +22 -10
  14. package/skills/debug-and-plan/SKILL.md +4 -4
  15. package/skills/plan-big/SKILL.md +5 -5
  16. package/skills/plan-normal/SKILL.md +5 -5
  17. package/skills/plan-small/SKILL.md +5 -5
  18. package/skills/plan-with-refs/SKILL.md +8 -8
  19. package/skills/planning/SKILL.md +1 -1
  20. package/src/ask-form.ts +4 -4
  21. package/src/auditor.ts +126 -0
  22. package/src/auto-approve.ts +1 -1
  23. package/src/autocomplete.ts +19 -17
  24. package/src/code-graph/commands.ts +2 -2
  25. package/src/code-graph/community.ts +1 -1
  26. package/src/code-graph/paths.ts +1 -1
  27. package/src/code-graph/watch.ts +2 -2
  28. package/src/compaction.ts +3 -3
  29. package/src/config-command.ts +154 -76
  30. package/src/dashboard.ts +257 -0
  31. package/src/exec.ts +709 -705
  32. package/src/global-state.ts +304 -0
  33. package/src/guard.ts +16 -3
  34. package/src/messaging.ts +44 -0
  35. package/src/plan.ts +421 -112
  36. package/src/query-hook.ts +4 -4
  37. package/src/refine-prompts.ts +14 -72
  38. package/src/refine-ui-helpers.ts +24 -5
  39. package/src/refine-ui-state.ts +1 -1
  40. package/src/refine-ui.ts +1 -1
  41. package/src/resume-command.ts +40 -130
  42. package/src/resume.ts +15 -17
  43. package/src/role-panels.ts +542 -0
  44. package/src/run-context.ts +5 -4
  45. package/src/run-picker.ts +98 -0
  46. package/src/state.ts +380 -77
  47. package/src/subagent.ts +32 -1
  48. package/src/task-tool.ts +100 -0
  49. package/src/tasks.ts +189 -0
  50. package/src/thinking-levels.ts +67 -0
  51. package/src/ui-language.ts +3 -54
  52. package/src/workflow-state.ts +78 -57
  53. package/tests/analyze-refs.test.ts +35 -18
  54. package/tests/ask-choice-pros-cons.test.ts +147 -0
  55. package/tests/ask-choice-schema.test.ts +0 -12
  56. package/tests/ask-choice.test.ts +2 -49
  57. package/tests/ask-form-tool.test.ts +4 -5
  58. package/tests/ask-form.test.ts +2 -2
  59. package/tests/auditor.test.ts +111 -0
  60. package/tests/auto-approve.test.ts +7 -10
  61. package/tests/autocomplete.test.ts +8 -11
  62. package/tests/code-graph-apply-action.test.ts +2 -2
  63. package/tests/code-graph-commands.test.ts +2 -2
  64. package/tests/code-graph-index.test.ts +2 -2
  65. package/tests/code-graph-loop.e2e.test.ts +1 -1
  66. package/tests/code-graph-mutations.test.ts +1 -1
  67. package/tests/code-graph-rollback.test.ts +1 -1
  68. package/tests/code-graph-v05.test.ts +2 -2
  69. package/tests/compaction.test.ts +1 -1
  70. package/tests/config-command.test.ts +103 -100
  71. package/tests/dashboard.test.ts +268 -0
  72. package/tests/exec-lifecycle.test.ts +181 -115
  73. package/tests/exec-panel-lifecycle.test.ts +106 -251
  74. package/tests/exec.test.ts +617 -1706
  75. package/tests/execute-plan.test.ts +44 -19
  76. package/tests/extension-load.test.ts +48 -0
  77. package/tests/global-state.test.ts +371 -0
  78. package/tests/graph-aware-file-tools.test.ts +5 -5
  79. package/tests/guard.test.ts +1 -1
  80. package/tests/multi-run.test.ts +184 -0
  81. package/tests/plan.test.ts +139 -62
  82. package/tests/plans.test.ts +7 -79
  83. package/tests/refine-prompts.test.ts +20 -71
  84. package/tests/refine-resume.test.ts +27 -22
  85. package/tests/refine-ui.test.ts +6 -15
  86. package/tests/resume-lifecycle.test.ts +37 -22
  87. package/tests/resume.test.ts +43 -88
  88. package/tests/role-panels.test.ts +391 -0
  89. package/tests/run-context.test.ts +1 -1
  90. package/tests/run-ownership.test.ts +1 -1
  91. package/tests/stale-ctx.test.ts +218 -0
  92. package/tests/state.test.ts +151 -32
  93. package/tests/subagent-thinking.test.ts +65 -0
  94. package/tests/subagent-usage.test.ts +1 -1
  95. package/tests/task-tool.test.ts +61 -0
  96. package/tests/thinking-levels.test.ts +77 -0
  97. package/tests/ui-language.test.ts +2 -17
  98. package/tests/workflow-state.test.ts +17 -99
  99. package/tools/analyze-refs.ts +67 -32
  100. package/tools/ask-choice.ts +19 -49
  101. package/tools/code-graph.ts +2 -2
  102. package/tools/execute-plan.ts +63 -33
  103. package/tools/graph-aware-file-tools.ts +6 -4
  104. package/tools/plans.ts +40 -66
  105. package/tools/refine.ts +101 -164
  106. package/agents/criticizer.md +0 -18
  107. package/scripts/bench/pi-adapter/__pycache__/pi_plans_bench.cpython-312.pyc +0 -0
  108. package/src/panel.ts +0 -473
  109. package/src/termination-prompt.ts +0 -73
  110. package/tests/goal-wait.test.ts +0 -269
  111. package/tests/panel-i-zero.test.ts +0 -420
  112. 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), "pi_plans");
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", "pi_plans");
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", "pi_plans");
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", "pi_plans");
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), "pi_plans");
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/pi_plans/.
366
- const dbPath = path.join(root, ".git", "pi_plans", "code_graph.db");
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 });
@@ -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 implementation item: I-002/);
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", "pi_plans", "config.json");
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", "pi_plans", "runs", runId, "run.json");
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 full wizard into config.json", async () => {
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
- if (question === "Reviewer model?") return labels.find((label) => label.includes("Use current session model (live/pro)"));
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, "en");
121
- assert.equal(config.language.source, "user");
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.refs_root_source, "user");
126
- assert.ok(config.refs_root_updated_at);
127
- assert.equal(config.graph_enabled, false);
128
- assert.equal(config.reviewer.mode, "current-session");
129
- assert.equal(config.reviewer.model_selector, "live/pro");
130
- assert.equal(config.criticizer.mode, "delegated-subagent");
131
- assert.equal(config.criticizer.model_selector, "custom/critic-model");
132
- assert.ok(config.reviewer.confirmed_at);
133
- assert.ok(config.criticizer.confirmed_at);
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("dedupes model choices and falls back to the live model when no default is stored", async () => {
137
- const dir = workdir("model-menu");
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?") return labels[0];
152
- if (question === "Criticizer mode?") return undefined;
153
- return undefined;
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 reviewerModelPrompt = recorded.selects.find((entry) => entry.question === "Reviewer model?");
160
- assert.ok(reviewerModelPrompt);
161
- assert.deepEqual(reviewerModelPrompt?.labels, [
162
- "1. Keep current default (inherit live model: live/pro)",
163
- "2. Use current session model (live/pro)",
164
- "3. Use available model (scoped/model-a)",
165
- "4. Use available model (registry/model-b)",
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("rejects an invalid model selector entered through Other and leaves config untouched", async () => {
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?") return labels.find((label) => label.includes("Other..."));
182
- return undefined;
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(configPath(dir)), false);
190
- assert.equal(recorded.notifies.some((entry) => entry.message.includes("Model selector must be an exact provider/model string.")), true);
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
- if (question === "Criticizer mode?") return labels.find((label) => label.includes("Keep delegated-subagent"));
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.language.tag, "en");
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("en"));
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(readJson(configPath(dir)).language.tag, "en", "the wizard must keep saving");
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,