pi-plans 0.1.2 → 0.2.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.
@@ -22,15 +22,16 @@ describe("plans tool source", () => {
22
22
 
23
23
  const params = source.slice(start, end);
24
24
  assert.match(params, /artifactRootSource:\s*Type\.Optional/);
25
- assert.match(params, /executionModelSelector:\s*Type\.Optional/);
26
- assert.match(params, /executionModelSource:\s*Type\.Optional/);
25
+ assert.doesNotMatch(params, /executionModelSelector/);
26
+ assert.doesNotMatch(params, /executionModelSource/);
27
27
  });
28
28
 
29
- it("keeps the plans handler wired to artifact root and execution model actions", () => {
29
+ it("keeps the plans handler wired to artifact root and no separate model-selection action", () => {
30
30
  const source = readPlansSource();
31
- assert.match(source, /import \{[\s\S]*setArtifactRoot,[\s\S]*setExecutionModel,[\s\S]*\} from "\.\.\/src\/state\.ts";/);
31
+ assert.match(source, /import \{[\s\S]*setArtifactRoot,[\s\S]*\} from "\.\.\/src\/state\.ts";/);
32
32
  assert.match(source, /case "set-artifact-root"/);
33
- assert.match(source, /case "set-execution-model"/);
33
+ assert.doesNotMatch(source, /setExecutionModel/);
34
+ assert.doesNotMatch(source, /case "set-execution-model"/);
34
35
  assert.match(source, /params\.artifactRootSource/);
35
36
  });
36
37
  });
@@ -0,0 +1,82 @@
1
+ import * as assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import {
4
+ consumeOrdinaryQuery,
5
+ isOrdinaryExternalQuery,
6
+ QUERY_INTERVIEW_MESSAGE,
7
+ QUERY_INTERVIEW_MESSAGE_CUSTOM_TYPE,
8
+ recordOrdinaryQuery,
9
+ registerQueryInterviewHooks,
10
+ resetOrdinaryQueryState,
11
+ } from "../src/query-hook.ts";
12
+
13
+ function context(sessionManager: object): any {
14
+ return { cwd: process.cwd(), sessionManager };
15
+ }
16
+
17
+ function registeredHooks(): { pi: any; handlers: Map<string, Function[]> } {
18
+ const handlers = new Map<string, Function[]>();
19
+ const pi = {
20
+ on(name: string, handler: Function) {
21
+ handlers.set(name, [...(handlers.get(name) ?? []), handler]);
22
+ },
23
+ };
24
+ return { pi, handlers };
25
+ }
26
+
27
+ const ordinaryInput = { text: "implement the requested change", source: "interactive" as const };
28
+
29
+ describe("query interview hook", () => {
30
+ it("accepts only idle external non-slash input", () => {
31
+ assert.equal(isOrdinaryExternalQuery(ordinaryInput), true);
32
+ assert.equal(isOrdinaryExternalQuery({ ...ordinaryInput, source: "rpc" }), true);
33
+ assert.equal(isOrdinaryExternalQuery({ ...ordinaryInput, source: "extension" }), false);
34
+ assert.equal(isOrdinaryExternalQuery({ ...ordinaryInput, streamingBehavior: "followUp" }), false);
35
+ assert.equal(isOrdinaryExternalQuery({ ...ordinaryInput, streamingBehavior: "steer" }), false);
36
+ assert.equal(isOrdinaryExternalQuery({ ...ordinaryInput, text: " " }), false);
37
+ assert.equal(isOrdinaryExternalQuery({ ...ordinaryInput, text: "/planning clarify this" }), false);
38
+ assert.equal(isOrdinaryExternalQuery({ ...ordinaryInput, text: "/skill:plan-small implement this" }), false);
39
+ assert.equal(isOrdinaryExternalQuery({ ...ordinaryInput, text: "/unknown-command" }), false);
40
+ });
41
+
42
+ it("queues multiple queries, consumes one at a time, and isolates sessions", () => {
43
+ const first = context({});
44
+ const second = context({});
45
+ assert.equal(recordOrdinaryQuery(first, ordinaryInput), true);
46
+ assert.equal(recordOrdinaryQuery(first, { ...ordinaryInput, text: "verify the change" }), true);
47
+ assert.equal(consumeOrdinaryQuery(second), false);
48
+ assert.equal(consumeOrdinaryQuery(first), true);
49
+ assert.equal(consumeOrdinaryQuery(first), true);
50
+ assert.equal(consumeOrdinaryQuery(first), false);
51
+ resetOrdinaryQueryState(first);
52
+ assert.equal(consumeOrdinaryQuery(first), false);
53
+ });
54
+
55
+ it("registers one hidden message per ordinary query and consumes suppressed queries", async () => {
56
+ const { pi, handlers } = registeredHooks();
57
+ let suppressed = false;
58
+ registerQueryInterviewHooks(pi, () => suppressed);
59
+ const session = {};
60
+ const ctx = context(session);
61
+ const input = handlers.get("input")![0]!;
62
+ const beforeAgentStart = handlers.get("before_agent_start")![0]!;
63
+ const sessionStart = handlers.get("session_start")![0]!;
64
+
65
+ await input({ ...ordinaryInput }, ctx);
66
+ const first = await beforeAgentStart({ type: "before_agent_start", prompt: ordinaryInput.text }, ctx);
67
+ assert.equal(first?.message.customType, QUERY_INTERVIEW_MESSAGE_CUSTOM_TYPE);
68
+ assert.equal(first?.message.display, false);
69
+ assert.equal(first?.message.content, QUERY_INTERVIEW_MESSAGE);
70
+ assert.equal(await beforeAgentStart({ type: "before_agent_start", prompt: ordinaryInput.text }, ctx), undefined);
71
+
72
+ await input({ ...ordinaryInput, text: "a query inside the planning workflow" }, ctx);
73
+ suppressed = true;
74
+ assert.equal(await beforeAgentStart({ type: "before_agent_start", prompt: "expanded workflow prompt" }, ctx), undefined);
75
+ suppressed = false;
76
+ assert.equal(await beforeAgentStart({ type: "before_agent_start", prompt: "later ordinary prompt" }, ctx), undefined);
77
+
78
+ await input({ ...ordinaryInput, text: "stale input" }, ctx);
79
+ await sessionStart({ type: "session_start" }, ctx);
80
+ assert.equal(await beforeAgentStart({ type: "before_agent_start", prompt: "new session" }, ctx), undefined);
81
+ });
82
+ });
@@ -0,0 +1,127 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import { RefineOverlayComponent, chunkDetailForOverlay } from "../src/refine-ui.ts";
6
+ import { applyRefineProgress, applyRefineResult, statusLabel, type RefineLaneState } from "../src/refine-ui-state.ts";
7
+
8
+
9
+ function lane(): RefineLaneState {
10
+ return { id: "lane-1", label: "reviewer-1", status: "queued", phase: "queued", detail: "" };
11
+ }
12
+
13
+ describe("refine overlay state", () => {
14
+ it("tracks tool progress and bounds the displayed detail", () => {
15
+ const state = lane();
16
+ applyRefineProgress(state, {
17
+ type: "tool",
18
+ phase: "update",
19
+ toolCallId: "call-1",
20
+ toolName: "read",
21
+ detail: "x".repeat(500),
22
+ });
23
+
24
+ assert.equal(state.status, "running");
25
+ assert.equal(state.phase, "tool: read");
26
+ assert.ok(state.detail.length <= 180);
27
+ assert.match(state.detail, /\.\.\.$/);
28
+ });
29
+
30
+ it("keeps terminal lane states stable after completion", () => {
31
+ const state = lane();
32
+ applyRefineResult(state, { ok: true, output: "final conclusion", stderr: "", turns: 1 });
33
+ applyRefineProgress(state, { type: "turn", phase: "start" });
34
+
35
+ assert.deepEqual(state, {
36
+ id: "lane-1",
37
+ label: "reviewer-1",
38
+ status: "complete",
39
+ phase: "complete",
40
+ detail: "final conclusion",
41
+ });
42
+ });
43
+
44
+ it("distinguishes cancelled and timed out child results", () => {
45
+ const cancelled = lane();
46
+ applyRefineResult(cancelled, { ok: false, output: "", stderr: "", turns: 0, cancelled: true, errorMessage: "Esc" });
47
+ assert.equal(cancelled.status, "cancelled");
48
+ assert.equal(cancelled.phase, "cancelled");
49
+
50
+ const timedOut = lane();
51
+ applyRefineResult(timedOut, { ok: false, output: "", stderr: "", turns: 0, timedOut: true, errorMessage: "timeout" });
52
+ assert.equal(timedOut.status, "failed");
53
+ assert.equal(timedOut.phase, "timed out");
54
+ assert.equal(statusLabel(timedOut.status), "failed");
55
+ });
56
+ });
57
+
58
+
59
+ describe("refine overlay viewport", () => {
60
+ const fakeTheme = {
61
+ fg: (_color: string, text: string) => text,
62
+ bold: (text: string) => text,
63
+ } as never;
64
+
65
+ function makeLane(): RefineLaneState {
66
+ return { id: "lane-1", label: "reviewer-1", status: "running", phase: "responding", detail: "" };
67
+ }
68
+
69
+ function makeLanes(detail: string): RefineLaneState[] {
70
+ return [{ id: "lane-1", label: "reviewer-1", status: "running", phase: "responding", detail }];
71
+ }
72
+
73
+ it("clamps overlay width between min and max regardless of host width", () => {
74
+ const component = new RefineOverlayComponent(fakeTheme, "reviewer", makeLanes(""), () => {});
75
+ const narrow = component.render(20);
76
+ const wide = component.render(240);
77
+ assert.ok(narrow.some((line) => line.startsWith("┌")));
78
+ assert.ok(wide.some((line) => line.startsWith("┌")));
79
+ assert.ok(narrow.every((line) => line.length <= 20));
80
+ });
81
+
82
+ it("draws top and bottom borders with rounded corners and frame lines", () => {
83
+ const component = new RefineOverlayComponent(fakeTheme, "criticizer", makeLanes(""), () => {});
84
+ const lines = component.render(80);
85
+ const first = lines[0] ?? "";
86
+ const last = lines.at(-1) ?? "";
87
+ assert.match(first, /^┌─+┐$/);
88
+ assert.match(last, /^└─+┘$/);
89
+ assert.ok(lines.some((line) => line.startsWith("├") && line.endsWith("┤")));
90
+ assert.ok(lines.some((line) => line.startsWith("│") && line.endsWith("│")));
91
+ });
92
+
93
+ it("chunk sentence-sized detail lines so streaming fragments stay readable", () => {
94
+ const lines = chunkDetailForOverlay(
95
+ "Reading the file. Found three findings. The fourth line is unrelated.",
96
+ 40,
97
+ 6,
98
+ );
99
+ assert.ok(lines.length >= 3);
100
+ assert.ok(lines.every((line) => line.length <= 40));
101
+ assert.match(lines[0] ?? "", /Reading the file\./);
102
+ });
103
+
104
+ it("limits chunk length and refuses to overflow the maxLines budget", () => {
105
+ const text = Array.from({ length: 30 }, (_, i) => `Sentence number ${i + 1}.`).join(" ");
106
+ const lines = chunkDetailForOverlay(text, 60, 3);
107
+ assert.equal(lines.length, 3);
108
+ });
109
+ });
110
+
111
+ describe("refine overlay wiring", () => {
112
+ it("uses named public overlays, guards lifecycle edges, and does not depend on pi-btw", () => {
113
+ const source = fs.readFileSync(path.join(process.cwd(), "src", "refine-ui.ts"), "utf8");
114
+ assert.match(source, /custom<void>/);
115
+ assert.match(source, /overlay:\s*true/);
116
+ assert.match(source, /"Reviewer"/);
117
+ assert.match(source, /"Criticizer"/);
118
+ assert.match(source, /truncateToWidth/);
119
+ assert.match(source, /chunkDetailForOverlay/);
120
+ assert.match(source, /pickOverlayHeight|pickOverlayWidth/);
121
+ assert.match(source, /renderBorderLine|renderHorizontalRule|renderRow/);
122
+ assert.match(source, /if \(this\.closed\) return;/);
123
+ assert.match(source, /await this\.overlayPromise/);
124
+ assert.match(source, /if \(!ctx\.hasUI \|\| this\.overlayPromise\) return/);
125
+ assert.doesNotMatch(source, /pi-btw|createAgentSession|AgentSession/);
126
+ });
127
+ });
@@ -13,7 +13,6 @@ import {
13
13
  recordDecision,
14
14
  setLanguage,
15
15
  setArtifactRoot,
16
- setExecutionModel,
17
16
  setRole,
18
17
  setRunStatus,
19
18
  showConfig,
@@ -69,7 +68,7 @@ describe("init", () => {
69
68
  assert.equal(config.reviewer.mode, "delegated-subagent");
70
69
  assert.equal(config.reviewer.confirmed_at, null);
71
70
  assert.equal(config.criticizer.confirmed_at, null);
72
- assert.equal(config.execution.model_selector, null);
71
+ assert.equal("execution" in config, false);
73
72
  assert.equal(config.artifact_root, "./docs/pi-plans");
74
73
  assert.equal(config.artifact_root_source, "unset");
75
74
  assert.equal(config.artifact_root_updated_at, null);
@@ -238,21 +237,19 @@ describe("runs", () => {
238
237
  });
239
238
  });
240
239
 
241
- describe("execution model config", () => {
242
- it("roundtrips the selector and clears on inherit", () => {
243
- const workdir = mkWorkdir("exec-model");
240
+ describe("legacy execution config", () => {
241
+ it("strips legacy execution config on load and rewrite", () => {
242
+ const workdir = mkWorkdir("legacy-exec");
244
243
  git(workdir, "init", "-q");
245
244
  initState(workdir);
246
- assert.equal(readConfig(workdir).execution.model_selector, null);
247
-
248
- setExecutionModel(workdir, { modelSelector: "zai/glm-5.3-flash:high", source: "user" });
249
- assert.equal(readConfig(workdir).execution.model_selector, "zai/glm-5.3-flash:high");
250
- assert.equal(readConfig(workdir).execution.source, "user");
251
- assert.equal(showConfig(workdir).execution.model_selector, "zai/glm-5.3-flash:high");
252
-
253
- setExecutionModel(workdir, { modelSelector: "inherit", source: "user" });
254
- assert.equal(readConfig(workdir).execution.model_selector, null);
255
- assert.equal(readConfig(workdir).execution.source, "user");
245
+ const configPath = path.join(commonDir(workdir), "pi_plans", "config.json");
246
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
247
+ config.execution = { model_selector: "zai/glm-5.3-flash:high", source: "user", updated_at: null };
248
+ fs.writeFileSync(configPath, `${JSON.stringify(config, null, "\t")}\n`, "utf8");
249
+ const shown = showConfig(workdir) as any;
250
+ assert.equal(shown.execution, undefined);
251
+ setLanguage(workdir, "zh-Hans", "user");
252
+ assert.equal(readConfig(workdir).execution, undefined);
256
253
  });
257
254
  });
258
255
 
@@ -0,0 +1,114 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import * as fs from "node:fs";
4
+ import * as os from "node:os";
5
+ import * as path from "node:path";
6
+ import { normalizeSubagentEvent, runPiSubagent, type SubagentProgressEvent } from "../src/subagent.ts";
7
+
8
+ async function withFakePi(
9
+ task: string,
10
+ options: { signal?: AbortSignal; timeoutMs?: number; onProgress?: (event: SubagentProgressEvent) => void } = {},
11
+ ) {
12
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-plans-fake-pi-"));
13
+ const script = path.join(dir, "fake-pi.mjs");
14
+ fs.writeFileSync(
15
+ script,
16
+ [
17
+ 'const emit = (event) => process.stdout.write(JSON.stringify(event) + "\\n");',
18
+ 'emit({ type: "turn_start" });',
19
+ 'if (process.argv.includes("Task: slow")) await new Promise((resolve) => setTimeout(resolve, 5000));',
20
+ 'else {',
21
+ ' emit({ type: "message_start", message: { role: "assistant", content: [] } });',
22
+ ' emit({ type: "message_update", assistantMessageEvent: { type: "text_delta", delta: "working" } });',
23
+ ' emit({ type: "tool_execution_start", toolCallId: "call-1", toolName: "read", args: { path: "PLAN_v1.md" } });',
24
+ ' emit({ type: "message_end", message: { role: "assistant", model: "fake/model", content: [{ type: "text", text: "final conclusion" }] } });',
25
+ '}',
26
+ ].join("\n"),
27
+ );
28
+
29
+ const previousScript = process.argv[1];
30
+ process.argv[1] = script;
31
+ try {
32
+ return await runPiSubagent({
33
+ systemPrompt: "test system prompt",
34
+ task,
35
+ cwd: process.cwd(),
36
+ signal: options.signal,
37
+ timeoutMs: options.timeoutMs ?? 2000,
38
+ onProgress: options.onProgress,
39
+ });
40
+ } finally {
41
+ process.argv[1] = previousScript;
42
+ fs.rmSync(dir, { recursive: true, force: true });
43
+ }
44
+ }
45
+
46
+
47
+ describe("subagent runner lifecycle", () => {
48
+ it("waits for JSONL completion and forwards progress", async () => {
49
+ const progress: string[] = [];
50
+ const result = await withFakePi("normal", {
51
+ timeoutMs: 2000,
52
+ onProgress: (event) => progress.push(`${event.type}:${event.phase ?? ""}`),
53
+ });
54
+ assert.equal(result.ok, true);
55
+ assert.equal(result.output, "final conclusion");
56
+ assert.equal(result.model, "fake/model");
57
+ assert.ok(progress.includes("process:started"));
58
+ assert.ok(progress.includes("tool:start"));
59
+ assert.ok(progress.includes("process:exited"));
60
+ assert.equal(result.turns, 1);
61
+ });
62
+
63
+ it("returns a cancelled result after aborting the child", async () => {
64
+ const abort = new AbortController();
65
+ const promise = withFakePi("slow", { signal: abort.signal, timeoutMs: 2000 });
66
+ setTimeout(() => abort.abort(), 40);
67
+ const result = await promise;
68
+ assert.equal(result.ok, false);
69
+ assert.equal(result.cancelled, true);
70
+ assert.equal(result.timedOut, undefined);
71
+ });
72
+
73
+ it("returns a timed out result after terminating a slow child", async () => {
74
+ const result = await withFakePi("slow", { timeoutMs: 40 });
75
+ assert.equal(result.ok, false);
76
+ assert.equal(result.timedOut, true);
77
+ assert.equal(result.cancelled, undefined);
78
+ });
79
+ });
80
+
81
+ describe("subagent progress events", () => {
82
+ it("normalizes turn and message lifecycle events", () => {
83
+ assert.deepEqual(normalizeSubagentEvent({ type: "turn_start" }), { type: "turn", phase: "start" });
84
+ assert.deepEqual(
85
+ normalizeSubagentEvent({
86
+ type: "message_update",
87
+ assistantMessageEvent: { type: "text_delta", delta: "checking the plan" },
88
+ }),
89
+ { type: "message", phase: "update", role: "assistant", text: "checking the plan" },
90
+ );
91
+ });
92
+
93
+ it("normalizes tool progress without exposing unbounded arguments", () => {
94
+ const event = normalizeSubagentEvent({
95
+ type: "tool_execution_start",
96
+ toolCallId: "call-1",
97
+ toolName: "read",
98
+ args: { path: "/tmp/plan.md" },
99
+ });
100
+ assert.deepEqual(event, {
101
+ type: "tool",
102
+ phase: "start",
103
+ toolCallId: "call-1",
104
+ toolName: "read",
105
+ detail: '{"path":"/tmp/plan.md"}',
106
+ });
107
+ });
108
+
109
+ it("ignores unknown or malformed events", () => {
110
+ assert.equal(normalizeSubagentEvent(undefined), undefined);
111
+ assert.equal(normalizeSubagentEvent({ type: "future_event" }), undefined);
112
+ assert.equal(normalizeSubagentEvent({ type: "message_end" }), undefined);
113
+ });
114
+ });
@@ -14,6 +14,7 @@ import { StringEnum } from "@earendil-works/pi-ai";
14
14
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
15
  import { Text } from "@earendil-works/pi-tui";
16
16
  import { Type } from "typebox";
17
+ import { disableAutoComplete, enableAutoComplete, isAutoCompleteEnabled, recordAskChoice } from "../src/autocomplete.ts";
17
18
  import { normalizeWorkdir, readActive, recordDecision } from "../src/state.ts";
18
19
 
19
20
  const Option = Type.Object({
@@ -85,14 +86,28 @@ export function registerAskChoiceTool(pi: ExtensionAPI): void {
85
86
  source,
86
87
  });
87
88
 
89
+ // Once enabled for this planning run, eligible questions answer with the
90
+ // recommendation without opening another UI prompt.
91
+ if (autoComplete && isAutoCompleteEnabled(ctx)) {
92
+ recordAskChoice(ctx, true);
93
+ record(recommended.label, "auto-complete");
94
+ return {
95
+ content: [{ type: "text", text: `Auto-complete selected the recommended option: ${recommended.label}` }],
96
+ details: details(recommended.label, "auto-complete"),
97
+ };
98
+ }
99
+
88
100
  // No UI (print/json mode): planning questions may auto-complete;
89
101
  // questions without Auto-complete must stop and wait for the user.
90
102
  if (!ctx.hasUI) {
91
103
  if (!autoComplete) {
104
+ disableAutoComplete(ctx, "non-interactive boundary");
92
105
  throw new Error(
93
106
  "No UI available and this question must not be auto-completed (execution handoff or external-state change). Stop and wait for the user.",
94
107
  );
95
108
  }
109
+ enableAutoComplete(ctx);
110
+ recordAskChoice(ctx, true);
96
111
  record(recommended.label, "auto-complete");
97
112
  return {
98
113
  content: [
@@ -116,6 +131,7 @@ export function registerAskChoiceTool(pi: ExtensionAPI): void {
116
131
 
117
132
  const selected = await ctx.ui.select(params.question, displayLabels);
118
133
  if (selected === undefined) {
134
+ disableAutoComplete(ctx, "question cancelled");
119
135
  return {
120
136
  content: [
121
137
  {
@@ -128,6 +144,8 @@ export function registerAskChoiceTool(pi: ExtensionAPI): void {
128
144
  }
129
145
 
130
146
  if (autoComplete && selected.startsWith("Auto-complete")) {
147
+ enableAutoComplete(ctx);
148
+ recordAskChoice(ctx, true);
131
149
  record(recommended.label, "auto-complete");
132
150
  return {
133
151
  content: [
@@ -143,11 +161,13 @@ export function registerAskChoiceTool(pi: ExtensionAPI): void {
143
161
  if (allowOther && selected.startsWith("Other…")) {
144
162
  const typed = await ctx.ui.input(`${params.question} — your answer:`);
145
163
  if (typed === undefined || !typed.trim()) {
164
+ disableAutoComplete(ctx, "free-form answer cancelled");
146
165
  return {
147
166
  content: [{ type: "text", text: "User cancelled the free-form answer. Ask again or stop." }],
148
167
  details: details(null, "cancelled"),
149
168
  };
150
169
  }
170
+ recordAskChoice(ctx, false);
151
171
  const answer = typed.trim();
152
172
  record(answer, "user");
153
173
  return {
@@ -159,11 +179,13 @@ export function registerAskChoiceTool(pi: ExtensionAPI): void {
159
179
  const index = displayLabels.indexOf(selected);
160
180
  const option = index >= 0 && index < options.length ? options[index] : undefined;
161
181
  if (!option) {
182
+ recordAskChoice(ctx, false);
162
183
  return {
163
184
  content: [{ type: "text", text: `User selected: ${selected}` }],
164
185
  details: details(selected, "user"),
165
186
  };
166
187
  }
188
+ recordAskChoice(ctx, false);
167
189
  record(option.label, "user");
168
190
  return {
169
191
  content: [{ type: "text", text: `User selected: ${index + 1}. ${option.label}` }],
@@ -9,25 +9,12 @@ import { Type } from "typebox";
9
9
  import * as fs from "node:fs";
10
10
  import * as path from "node:path";
11
11
  import {
12
- chooseExecutionModelSelection,
13
- snapshotCurrentModelSelector,
14
12
  startExecution,
15
- type ExecutionModelState,
16
13
  } from "../src/exec.ts";
17
- import { extractCoverage, latestPlanVersion, parseChecklist, parseImplItems } from "../src/plan.ts";
18
- import { loadConfig, normalizeWorkdir, readActive, resolveStateRootOrNull, setExecutionModel, type ExecutionConfig } from "../src/state.ts";
14
+ import { disableAutoComplete } from "../src/autocomplete.ts";
15
+ import { latestPlanVersion, parseChecklist, parseImplItems } from "../src/plan.ts";
16
+ import { normalizeWorkdir, readActive } from "../src/state.ts";
19
17
 
20
- function loadExecutionModelConfig(workdir: string): ExecutionConfig {
21
- const root = resolveStateRootOrNull(workdir);
22
- if (!root) {
23
- return { model_selector: null, source: "unset", updated_at: null };
24
- }
25
- try {
26
- return loadConfig(root).execution;
27
- } catch {
28
- return { model_selector: null, source: "unset", updated_at: null };
29
- }
30
- }
31
18
 
32
19
  const ExecutePlanParams = Type.Object({
33
20
  planPath: Type.Optional(
@@ -82,6 +69,7 @@ export async function executeHandoff(
82
69
  }
83
70
  const implItems = parseImplItems(planText);
84
71
 
72
+ disableAutoComplete(ctx, "execution handoff");
85
73
  if (!ctx.hasUI) {
86
74
  return {
87
75
  status: "error",
@@ -99,33 +87,13 @@ export async function executeHandoff(
99
87
  return { status: "declined", message: "User declined execution. Stay in planning; ask how to proceed.", planPath };
100
88
  }
101
89
 
102
- const configuredExecution = loadExecutionModelConfig(workdir);
103
- let modelState: ExecutionModelState | undefined;
104
- const planningSelector = snapshotCurrentModelSelector(ctx);
105
- const selection = await chooseExecutionModelSelection(
106
- ctx,
107
- "Execution model",
108
- configuredExecution,
109
- async (selector) => {
110
- await setExecutionModel(workdir, { modelSelector: selector, source: "user" });
111
- },
112
- );
113
- if (!selection) {
114
- return { status: "error", message: "No usable execution model; execution cancelled." };
115
- }
116
- if (configuredExecution.source === "unset" || configuredExecution.model_selector !== null) {
117
- modelState = { planningSelector, executionSelector: selection.selector };
118
- }
119
-
120
- await startExecution(getCurrentApi(), ctx, planPath, items, modelState, implItems);
121
- const countedImpls = implItems.filter((impl) => items.some((item) => extractCoverage(item.text).includes(impl.id)));
122
- const modelNote = modelState ? ` Execution model: ${modelState.executionSelector}.` : "";
123
- const scopeNote = countedImpls.length ? ` Tracking ${countedImpls.length} implementation item(s).` : "";
90
+ await startExecution(getCurrentApi(), ctx, planPath, items, implItems);
91
+ const scopeNote = implItems.length ? ` Tracking ${implItems.length} implementation item(s).` : "";
124
92
  return {
125
93
  status: "executing",
126
94
  planPath,
127
95
  itemCount: items.length,
128
- message: `Execution approved. ${items.length} verifier item(s) queued; implement in dependency order and mark verified items with [DONE:VC-xxx].${scopeNote}${modelNote}`,
96
+ message: `Execution approved. ${items.length} verifier item(s) queued; implement in dependency order and mark verified items with [DONE:VC-xxx].${scopeNote}`,
129
97
  };
130
98
  }
131
99
 
package/tools/plans.ts CHANGED
@@ -15,7 +15,6 @@ import {
15
15
  recordSubagent,
16
16
  resolveStateRootOrNull,
17
17
  setArtifactRoot,
18
- setExecutionModel,
19
18
  setLanguage,
20
19
  setRunStatus,
21
20
  setRole,
@@ -32,7 +31,6 @@ const PlansParams = Type.Object({
32
31
  "show",
33
32
  "set-language",
34
33
  "set-artifact-root",
35
- "set-execution-model",
36
34
  "set-role",
37
35
  "start-run",
38
36
  "set-status",
@@ -52,10 +50,6 @@ const PlansParams = Type.Object({
52
50
  languageSource: Type.Optional(StringEnum(["user", "auto"] as const)),
53
51
  artifactRoot: Type.Optional(Type.String({ description: "set-artifact-root: planning docs root, e.g. ./docs/pi-plans" })),
54
52
  artifactRootSource: Type.Optional(StringEnum(["user", "auto"] as const)),
55
- executionModelSelector: Type.Optional(
56
- Type.String({ description: "set-execution-model: exact provider/model selector, or 'inherit' to clear" }),
57
- ),
58
- executionModelSource: Type.Optional(StringEnum(["user", "auto"] as const)),
59
53
  role: Type.Optional(StringEnum(["reviewer", "criticizer"] as const)),
60
54
  mode: Type.Optional(StringEnum(["delegated-subagent", "current-session"] as const)),
61
55
  modelSelector: Type.Optional(
@@ -120,7 +114,7 @@ export function registerPlansTool(pi: ExtensionAPI): void {
120
114
  name: "plans",
121
115
  label: "Plans",
122
116
  description:
123
- "Manage pi-plans planning state in the target workspace: init/show config, set language and planning docs root plus execution/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-execution-model, set-role, start-run, set-status, record-decision, record-ref, record-subagent.",
117
+ "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.",
124
118
  promptSnippet: "Manage pi-plans planning state, runs, and ledgers",
125
119
  parameters: PlansParams,
126
120
 
@@ -156,17 +150,6 @@ export function registerPlansTool(pi: ExtensionAPI): void {
156
150
  result = { config: updated.config, stateRoot: updated.stateRoot, notices: updated.notices };
157
151
  break;
158
152
  }
159
- case "set-execution-model": {
160
- if (!params.executionModelSelector || !params.executionModelSource) {
161
- throw new StateError("set-execution-model requires executionModelSelector and executionModelSource");
162
- }
163
- const updated = setExecutionModel(workdir, {
164
- modelSelector: params.executionModelSelector,
165
- source: params.executionModelSource,
166
- });
167
- result = { config: updated.config, stateRoot: updated.stateRoot, notices: updated.notices };
168
- break;
169
- }
170
153
  case "set-role": {
171
154
  if (!params.role) throw new StateError("set-role requires role");
172
155
  const updated = setRole(workdir, {