lsd-pi 1.2.2 → 1.2.3

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 (31) hide show
  1. package/dist/resources/extensions/slash-commands/plan.js +11 -7
  2. package/dist/resources/extensions/subagent/agents.js +17 -8
  3. package/dist/resources/extensions/subagent/index.js +112 -20
  4. package/package.json +1 -1
  5. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +1 -0
  6. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  7. package/packages/pi-coding-agent/dist/core/agent-session.js +53 -3
  8. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  9. package/packages/pi-coding-agent/dist/core/sdk.js +1 -1
  10. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  11. package/packages/pi-coding-agent/dist/core/skill-tool.test.js +15 -0
  12. package/packages/pi-coding-agent/dist/core/skill-tool.test.js.map +1 -1
  13. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  14. package/packages/pi-coding-agent/dist/core/skills.js +1 -0
  15. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  16. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  17. package/packages/pi-coding-agent/dist/core/system-prompt.js +1 -0
  18. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  19. package/packages/pi-coding-agent/dist/tests/path-display.test.js +1 -0
  20. package/packages/pi-coding-agent/dist/tests/path-display.test.js.map +1 -1
  21. package/packages/pi-coding-agent/package.json +1 -1
  22. package/packages/pi-coding-agent/src/core/agent-session.ts +51 -3
  23. package/packages/pi-coding-agent/src/core/sdk.ts +1 -1
  24. package/packages/pi-coding-agent/src/core/skill-tool.test.ts +18 -0
  25. package/packages/pi-coding-agent/src/core/skills.ts +1 -0
  26. package/packages/pi-coding-agent/src/core/system-prompt.ts +3 -0
  27. package/packages/pi-coding-agent/src/tests/path-display.test.ts +1 -0
  28. package/pkg/package.json +1 -1
  29. package/src/resources/extensions/slash-commands/plan.ts +14 -8
  30. package/src/resources/extensions/subagent/agents.ts +20 -6
  31. package/src/resources/extensions/subagent/index.ts +138 -18
@@ -13,7 +13,7 @@
13
13
  * Modes use this class and add their own I/O layer on top.
14
14
  */
15
15
 
16
- import { readFileSync } from "node:fs";
16
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
17
17
  import { basename, dirname, join } from "node:path";
18
18
  import { Text } from "@gsd/pi-tui";
19
19
  import type {
@@ -1001,7 +1001,52 @@ export class AgentSession {
1001
1001
  return this.resourceLoader.getSkills().skills.find((skill) => skill.name === skillName);
1002
1002
  }
1003
1003
 
1004
+ private _findKnownSubagentByName(agentName: string): { name: string; source: string } | undefined {
1005
+ const normalized = agentName.trim().toLowerCase();
1006
+ if (!normalized) return undefined;
1007
+
1008
+ const builtInAgentNames = new Set(["scout", "worker", "reviewer", "planner", "teams-builder", "teams-reviewer"]);
1009
+ if (builtInAgentNames.has(normalized)) {
1010
+ return { name: normalized, source: "bundled" };
1011
+ }
1012
+
1013
+ const userAgentsDir = join(process.env.LSD_CODING_AGENT_DIR || process.env.GSD_CODING_AGENT_DIR || join(process.env.HOME || "", ".lsd", "agent"), "agents");
1014
+ const projectCandidates = [".lsd", ".gsd", ".pi"].map((dir) => join(this._cwd, dir, "agents"));
1015
+ for (const [dirPath, source] of [[userAgentsDir, "user"], ...projectCandidates.map((p) => [p, "project"] as const)]) {
1016
+ if (!existsSync(dirPath)) continue;
1017
+ let entries: string[] = [];
1018
+ try {
1019
+ entries = readdirSync(dirPath);
1020
+ } catch {
1021
+ continue;
1022
+ }
1023
+ for (const entry of entries) {
1024
+ if (!entry.endsWith(".md")) continue;
1025
+ const filePath = join(dirPath, entry);
1026
+ try {
1027
+ if (!statSync(filePath).isFile()) continue;
1028
+ const raw = readFileSync(filePath, "utf-8");
1029
+ const match = raw.match(/^---\s*[\r\n]+([\s\S]*?)[\r\n]+---/);
1030
+ const frontmatter = match?.[1] ?? "";
1031
+ const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
1032
+ const name = nameMatch?.[1]?.trim().replace(/^['"]|['"]$/g, "");
1033
+ if (name?.toLowerCase() === normalized) {
1034
+ return { name, source };
1035
+ }
1036
+ } catch {
1037
+ continue;
1038
+ }
1039
+ }
1040
+ }
1041
+
1042
+ return undefined;
1043
+ }
1044
+
1004
1045
  private _formatMissingSkillMessage(skillName: string): string {
1046
+ const knownSubagent = this._findKnownSubagentByName(skillName);
1047
+ if (knownSubagent) {
1048
+ return `\"${knownSubagent.name}\" is an available subagent (${knownSubagent.source}), not a skill. Use the subagent tool directly with { agent: \"${knownSubagent.name}\", task: \"...\" }.`;
1049
+ }
1005
1050
  const availableSkills = this.resourceLoader.getSkills().skills.map((skill) => skill.name).join(", ") || "(none)";
1006
1051
  return `Skill "${skillName}" not found. Available skills: ${availableSkills}`;
1007
1052
  }
@@ -1059,6 +1104,7 @@ export class AgentSession {
1059
1104
  "Scout-first reconnaissance policy: when you need architecture context across multiple files or folders, do not map the subsystem by reading file-after-file yourself. Launch the scout subagent first, then continue with lsp and targeted reads.",
1060
1105
  "If your next step would be broad exploration rather than a targeted lookup, prefer scout before doing more read/find/grep sweeps yourself.",
1061
1106
  "When you choose scout, call subagent directly with valid parameters: { agent, task } for one scout, or { tasks: [{ agent, task }, ...] } for parallel scouts.",
1107
+ "If the user explicitly names a subagent such as scout, worker, reviewer, or planner, invoke the subagent tool directly rather than the Skill tool or ad-hoc discovery.",
1062
1108
  "Scout is reconnaissance-only. Do not use it as the reviewer, auditor, or final issue-ranker; use it to map files, ownership, and likely hotspots for later evaluation.",
1063
1109
  "If the work spans multiple loosely-coupled subsystems, prefer parallel scout subagents so each scout maps one area and the parent model reads the summaries instead of the raw files.",
1064
1110
  "For broad review or audit requests, use scout only as a prep step to map architecture and hotspots; the parent model or a reviewer should make the final judgments.",
@@ -1354,7 +1400,7 @@ export class AgentSession {
1354
1400
  name: "Skill",
1355
1401
  label: "Skill",
1356
1402
  description:
1357
- "Execute a skill within the main conversation. Use this tool when users ask for a slash command or reference a skill by name. Returns the expanded skill block and appends args after it.",
1403
+ "Execute a listed skill within the main conversation. Use this tool only for actual available skills or explicit /skill:name-style requests — not for launching subagents like scout, worker, reviewer, or planner. Returns the expanded skill block and appends args after it.",
1358
1404
  parameters: skillSchema,
1359
1405
  execute: async (_toolCallId, params: unknown) => {
1360
1406
  const input = params as { skill: string; args?: string };
@@ -2371,7 +2417,9 @@ export class AgentSession {
2371
2417
 
2372
2418
  const nextActiveToolNames = options?.activeToolNames
2373
2419
  ? [...options.activeToolNames]
2374
- : [...previousActiveToolNames];
2420
+ : this.settingsManager.getToolProfile() === "full"
2421
+ ? Array.from(toolRegistry.keys())
2422
+ : [...previousActiveToolNames];
2375
2423
 
2376
2424
  if (options?.includeAllExtensionTools) {
2377
2425
  for (const tool of wrappedExtensionTools) {
@@ -279,7 +279,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
279
279
  ? ["hashline_read", "bash", "hashline_edit", "write", "lsp", "bg_shell", "tool_search", "tool_enable", "Skill", "subagent", "await_subagent", "ask_user_questions"]
280
280
  : ["read", "bash", "edit", "write", "lsp", "bg_shell", "tool_search", "tool_enable", "Skill", "subagent", "await_subagent", "ask_user_questions"];
281
281
  const defaultActiveToolNames: string[] = toolProfile === "full"
282
- ? Object.keys(allTools)
282
+ ? []
283
283
  : balancedToolNames;
284
284
  const initialActiveToolNames: string[] = options.tools
285
285
  ? options.tools.map((t) => t.name).filter((n): n is string => typeof n === "string" && n in allTools)
@@ -85,6 +85,24 @@ describe("Skill tool", () => {
85
85
  );
86
86
  });
87
87
 
88
+ it("describes Skill as distinct from subagents", async () => {
89
+ const session = await createSession();
90
+ const tool = session.state.tools.find((entry) => entry.name === "Skill");
91
+ assert.ok(tool, "Skill tool should be registered");
92
+ assert.match(tool.description ?? "", /not for launching subagents like scout, worker, reviewer, or planner/i);
93
+ });
94
+
95
+ it("returns a helpful redirect when a known subagent name is passed to Skill", async () => {
96
+ const session = await createSession();
97
+ const tool = session.state.tools.find((entry) => entry.name === "Skill");
98
+ assert.ok(tool, "Skill tool should be registered");
99
+
100
+ const result = await tool.execute("call-subagent", { skill: "scout" });
101
+ const message = result.content[0]?.type === "text" ? result.content[0].text : "";
102
+ assert.match(message, /"scout" is an available subagent/i);
103
+ assert.match(message, /Use the subagent tool directly with \{ agent: "scout", task: "\.\.\." \}\./i);
104
+ });
105
+
88
106
  it("returns a helpful error for unknown skills", async () => {
89
107
  writeSkill(testDir, "swift-testing", "Use for Swift Testing assertions and verification patterns.");
90
108
  const session = await createSession();
@@ -324,6 +324,7 @@ export function formatSkillsForPrompt(skills: Skill[]): string {
324
324
  const lines = [
325
325
  "\n\nThe following skills provide specialized instructions for specific tasks.",
326
326
  "Use the Skill tool with the exact skill name from <available_skills> when the task matches its description.",
327
+ "Use the Skill tool only for listed skills — not for launching subagents like scout, worker, reviewer, or planner.",
327
328
  "If the Skill tool reports an unknown skill, do not guess: use an exact name from <available_skills> or tell the user the skill is unavailable.",
328
329
  "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
329
330
  "",
@@ -172,6 +172,9 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
172
172
  addGuideline(
173
173
  "Call the subagent tool directly. For one scout use { agent, task }. For several scouts use parallel mode with { tasks: [{ agent, task }, ...] }.",
174
174
  );
175
+ addGuideline(
176
+ "When the user names a subagent such as scout, worker, reviewer, or planner, invoke the subagent tool directly rather than the Skill tool or ad-hoc search.",
177
+ );
175
178
  addGuideline(
176
179
  "Scout is for mapping and reconnaissance only — not for final review, audit, or ranked issue lists. Use it to identify relevant files, subsystems, and likely hotspots for later evaluation.",
177
180
  );
@@ -74,6 +74,7 @@ test("buildSystemPrompt: encourages scout-first reconnaissance when subagent is
74
74
  assert.match(prompt, /multiple scout subagents in parallel/i);
75
75
  assert.match(prompt, /broad review or audit requests, use scout only as a prep step/i);
76
76
  assert.match(prompt, /Skip scout only when the task is clearly narrow/i);
77
+ assert.match(prompt, /When the user names a subagent such as scout, worker, reviewer, or planner, invoke the subagent tool directly/i);
77
78
  });
78
79
 
79
80
  // ─── Regression: no backslash paths in LLM-visible text ────────────────────
package/pkg/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lsd-pi",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "piConfig": {
5
5
  "name": "lsd",
6
6
  "configDir": ".lsd"
@@ -275,15 +275,21 @@ async function enablePlanModeWithModelSwitch(
275
275
  next: Partial<Pick<PlanModeState, "task" | "latestPlanPath" | "approvalStatus" | "previousMode" | "preplanModel" | "targetPermissionMode">> = {},
276
276
  ): Promise<void> {
277
277
  enablePlanMode(pi, currentModel, next);
278
- // Signal that before_agent_start should switch to the reasoning model on next turn
278
+ // Keep fallback behavior in before_agent_start for restored sessions or when
279
+ // immediate switching cannot be completed at entry-time.
279
280
  reasoningModelSwitchDone = false;
280
281
  if (!readAutoSwitchPlanModelSetting()) return;
281
- if (!readPlanModeReasoningModel()) {
282
+
283
+ const reasoningModel = parseQualifiedModelRef(readPlanModeReasoningModel());
284
+ if (!reasoningModel) {
282
285
  ctx.ui?.notify?.(
283
286
  "OpusPlan: set a Plan reasoning model in /settings to auto-switch on entry",
284
287
  "info",
285
288
  );
289
+ return;
286
290
  }
291
+
292
+ reasoningModelSwitchDone = await setModelIfNeeded(pi, ctx, reasoningModel);
287
293
  }
288
294
 
289
295
  function enablePlanMode(
@@ -329,13 +335,14 @@ function leavePlanMode(
329
335
  return nextPermissionMode;
330
336
  }
331
337
 
332
- async function setModelIfNeeded(pi: ExtensionAPI, ctx: any, modelRef: ModelRef | undefined): Promise<void> {
333
- if (!modelRef) return;
338
+ async function setModelIfNeeded(pi: ExtensionAPI, ctx: any, modelRef: ModelRef | undefined): Promise<boolean> {
339
+ if (!modelRef) return false;
334
340
  const currentModel = parseQualifiedModelRef(ctx?.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined);
335
- if (sameModel(currentModel, modelRef)) return;
341
+ if (sameModel(currentModel, modelRef)) return true;
336
342
  const model = resolveModelFromContext(ctx, modelRef);
337
- if (!model) return;
343
+ if (!model) return false;
338
344
  await pi.setModel(model, { persist: false });
345
+ return true;
339
346
  }
340
347
 
341
348
  function buildExecutionKickoffMessage(options: { permissionMode: RestorablePermissionMode; executeWithSubagent?: boolean }): string {
@@ -640,8 +647,7 @@ export default function planCommand(pi: ExtensionAPI) {
640
647
  if (!reasoningModelSwitchDone && readAutoSwitchPlanModelSetting()) {
641
648
  const reasoningModel = parseQualifiedModelRef(readPlanModeReasoningModel());
642
649
  if (reasoningModel) {
643
- reasoningModelSwitchDone = true;
644
- await setModelIfNeeded(pi, ctx, reasoningModel);
650
+ reasoningModelSwitchDone = await setModelIfNeeded(pi, ctx, reasoningModel);
645
651
  }
646
652
  }
647
653
  return { systemPrompt: buildPlanModeSystemPrompt() };
@@ -4,6 +4,7 @@
4
4
 
5
5
  import * as fs from "node:fs";
6
6
  import * as path from "node:path";
7
+ import { fileURLToPath } from "node:url";
7
8
  import { getAgentDir, parseFrontmatter } from "@gsd/pi-coding-agent";
8
9
 
9
10
  const PROJECT_AGENT_DIR_CANDIDATES = [".lsd", ".gsd", ".pi"] as const;
@@ -16,7 +17,7 @@ export interface AgentConfig {
16
17
  tools?: string[];
17
18
  model?: string;
18
19
  systemPrompt: string;
19
- source: "user" | "project";
20
+ source: "bundled" | "user" | "project";
20
21
  filePath: string;
21
22
  }
22
23
 
@@ -35,7 +36,7 @@ function normalizeAgentModel(model: string | undefined): string | undefined {
35
36
  return parts.length === 2 && parts.every(Boolean) ? trimmed : undefined;
36
37
  }
37
38
 
38
- function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
39
+ function loadAgentsFromDir(dir: string, source: "bundled" | "user" | "project"): AgentConfig[] {
39
40
  const agents: AgentConfig[] = [];
40
41
 
41
42
  if (!fs.existsSync(dir)) {
@@ -110,22 +111,35 @@ function findNearestProjectAgentsDir(cwd: string): string | null {
110
111
  }
111
112
  }
112
113
 
114
+ function getBundledAgentsDir(): string {
115
+ const here = path.dirname(fileURLToPath(import.meta.url));
116
+ return path.resolve(here, "../../agents");
117
+ }
118
+
113
119
  export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
114
120
  const userDir = path.join(getAgentDir(), "agents");
121
+ const bundledDir = getBundledAgentsDir();
115
122
  const projectAgentsDir = findNearestProjectAgentsDir(cwd);
116
123
 
124
+ const bundledAgents = scope === "project" ? [] : loadAgentsFromDir(bundledDir, "bundled");
117
125
  const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
118
126
  const projectAgents = scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
119
127
 
120
128
  const agentMap = new Map<string, AgentConfig>();
121
129
 
130
+ const addAgents = (items: AgentConfig[]) => {
131
+ for (const agent of items) agentMap.set(agent.name, agent);
132
+ };
133
+
122
134
  if (scope === "both") {
123
- for (const agent of userAgents) agentMap.set(agent.name, agent);
124
- for (const agent of projectAgents) agentMap.set(agent.name, agent);
135
+ addAgents(bundledAgents);
136
+ addAgents(userAgents);
137
+ addAgents(projectAgents);
125
138
  } else if (scope === "user") {
126
- for (const agent of userAgents) agentMap.set(agent.name, agent);
139
+ addAgents(bundledAgents);
140
+ addAgents(userAgents);
127
141
  } else {
128
- for (const agent of projectAgents) agentMap.set(agent.name, agent);
142
+ addAgents(projectAgents);
129
143
  }
130
144
 
131
145
  return { agents: Array.from(agentMap.values()), projectAgentsDir };
@@ -25,9 +25,9 @@ import {
25
25
  getAgentDir,
26
26
  getMarkdownTheme,
27
27
  } from "@gsd/pi-coding-agent";
28
- import { Container, Markdown, Spacer, Text } from "@gsd/pi-tui";
28
+ import { Container, Key, Markdown, Spacer, Text } from "@gsd/pi-tui";
29
29
  import { Type } from "@sinclair/typebox";
30
- import { formatTokenCount } from "../shared/mod.js";
30
+ import { formatTokenCount, shortcutDesc } from "../shared/mod.js";
31
31
  import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js";
32
32
  import { buildSubagentProcessArgs, getBundledExtensionPathsFromEnv } from "./launch-helpers.js";
33
33
  import {
@@ -314,7 +314,7 @@ interface UsageStats {
314
314
 
315
315
  interface SingleResult {
316
316
  agent: string;
317
- agentSource: "user" | "project" | "unknown";
317
+ agentSource: "bundled" | "user" | "project" | "unknown";
318
318
  task: string;
319
319
  exitCode: number;
320
320
  messages: Message[];
@@ -327,6 +327,20 @@ interface SingleResult {
327
327
  backgroundJobId?: string;
328
328
  }
329
329
 
330
+ interface ForegroundSingleRunControl {
331
+ agentName: string;
332
+ task: string;
333
+ cwd: string;
334
+ abortController: AbortController;
335
+ resultPromise: Promise<{ summary: string; stderr: string; exitCode: number; model?: string }>;
336
+ adoptToBackground: (jobId: string) => boolean;
337
+ }
338
+
339
+ interface ForegroundSingleRunHooks {
340
+ onStart?: (control: ForegroundSingleRunControl) => void;
341
+ onFinish?: () => void;
342
+ }
343
+
330
344
  interface SubagentDetails {
331
345
  mode: "single" | "parallel" | "chain";
332
346
  agentScope: AgentScope;
@@ -501,7 +515,8 @@ async function runSingleAgent(
501
515
  parentModel: { provider: string; id: string } | undefined,
502
516
  signal: AbortSignal | undefined,
503
517
  onUpdate: OnUpdateCallback | undefined,
504
- makeDetails: (results: SingleResult[]) => SubagentDetails
518
+ makeDetails: (results: SingleResult[]) => SubagentDetails,
519
+ foregroundHooks?: ForegroundSingleRunHooks,
505
520
  ): Promise<SingleResult> {
506
521
  const agent = agents.find((a) => a.name === agentName);
507
522
 
@@ -551,6 +566,27 @@ async function runSingleAgent(
551
566
  }
552
567
  };
553
568
 
569
+ let wasAborted = false;
570
+ let deferTempPromptCleanup = false;
571
+ let tempPromptCleanupDone = false;
572
+
573
+ const cleanupTempPromptFiles = () => {
574
+ if (tempPromptCleanupDone) return;
575
+ tempPromptCleanupDone = true;
576
+ if (tmpPromptPath)
577
+ try {
578
+ fs.unlinkSync(tmpPromptPath);
579
+ } catch {
580
+ /* ignore */
581
+ }
582
+ if (tmpPromptDir)
583
+ try {
584
+ fs.rmdirSync(tmpPromptDir);
585
+ } catch {
586
+ /* ignore */
587
+ }
588
+ };
589
+
554
590
  try {
555
591
  if (agent.systemPrompt.trim()) {
556
592
  const tmp = writePromptToTempFile(agent.name, agent.systemPrompt);
@@ -558,7 +594,6 @@ async function runSingleAgent(
558
594
  tmpPromptPath = tmp.filePath;
559
595
  }
560
596
  const args = buildSubagentProcessArgs(agent, task, tmpPromptPath, inferredModel);
561
- let wasAborted = false;
562
597
 
563
598
  const exitCode = await new Promise<number>((resolve) => {
564
599
  const bundledPaths = getBundledExtensionPathsFromEnv();
@@ -581,6 +616,7 @@ async function runSingleAgent(
581
616
  let buffer = "";
582
617
  let completionSeen = false;
583
618
  let resolved = false;
619
+ let foregroundReleased = false;
584
620
  const procAbortController = new AbortController();
585
621
  let resolveBackgroundResult: ((value: { summary: string; stderr: string; exitCode: number; model?: string }) => void) | undefined;
586
622
  let rejectBackgroundResult: ((reason?: unknown) => void) | undefined;
@@ -595,7 +631,27 @@ async function runSingleAgent(
595
631
  resolve(code);
596
632
  };
597
633
 
634
+ const adoptToBackground = (jobId: string): boolean => {
635
+ if (resolved || foregroundReleased) return false;
636
+ foregroundReleased = true;
637
+ deferTempPromptCleanup = true;
638
+ currentResult.backgroundJobId = jobId;
639
+ finishForeground(0);
640
+ return true;
641
+ };
598
642
 
643
+ backgroundResultPromise.finally(() => {
644
+ if (deferTempPromptCleanup) cleanupTempPromptFiles();
645
+ });
646
+
647
+ foregroundHooks?.onStart?.({
648
+ agentName,
649
+ task,
650
+ cwd: cwd ?? defaultCwd,
651
+ abortController: procAbortController,
652
+ resultPromise: backgroundResultPromise,
653
+ adoptToBackground,
654
+ });
599
655
 
600
656
  proc.stdout.on("data", (data) => {
601
657
  buffer += data.toString();
@@ -631,12 +687,14 @@ async function runSingleAgent(
631
687
  exitCode: finalExitCode,
632
688
  model: currentResult.model,
633
689
  });
690
+ foregroundHooks?.onFinish?.();
634
691
  finishForeground(finalExitCode);
635
692
  });
636
693
 
637
694
  proc.on("error", (error) => {
638
695
  liveSubagentProcesses.delete(proc);
639
696
  rejectBackgroundResult?.(error);
697
+ foregroundHooks?.onFinish?.();
640
698
  finishForeground(1);
641
699
  });
642
700
 
@@ -670,18 +728,7 @@ async function runSingleAgent(
670
728
  if (wasAborted) throw new Error("Subagent was aborted");
671
729
  return currentResult;
672
730
  } finally {
673
- if (tmpPromptPath)
674
- try {
675
- fs.unlinkSync(tmpPromptPath);
676
- } catch {
677
- /* ignore */
678
- }
679
- if (tmpPromptDir)
680
- try {
681
- fs.rmdirSync(tmpPromptDir);
682
- } catch {
683
- /* ignore */
684
- }
731
+ if (!deferTempPromptCleanup) cleanupTempPromptFiles();
685
732
  }
686
733
  }
687
734
 
@@ -754,6 +801,10 @@ const SubagentParams = Type.Object({
754
801
 
755
802
  export default function(pi: ExtensionAPI) {
756
803
  let bgManager: BackgroundJobManager | null = null;
804
+ const foregroundSubagentStatusKey = "foreground-subagent";
805
+ const foregroundSubagentHint = "Ctrl+B: move foreground subagent to background";
806
+ type ActiveForegroundSubagent = ForegroundSingleRunControl & { claimed: boolean };
807
+ let activeForegroundSubagent: ActiveForegroundSubagent | null = null;
757
808
 
758
809
  function getBgManager(): BackgroundJobManager {
759
810
  if (!bgManager) throw new Error("BackgroundJobManager not initialized.");
@@ -792,6 +843,7 @@ export default function(pi: ExtensionAPI) {
792
843
 
793
844
 
794
845
  pi.on("session_before_switch", async () => {
846
+ activeForegroundSubagent = null;
795
847
  if (bgManager) {
796
848
  for (const job of bgManager.getRunningJobs()) {
797
849
  bgManager.cancel(job.id);
@@ -800,6 +852,7 @@ export default function(pi: ExtensionAPI) {
800
852
  });
801
853
 
802
854
  pi.on("session_shutdown", async () => {
855
+ activeForegroundSubagent = null;
803
856
  await stopLiveSubagents();
804
857
  if (bgManager) {
805
858
  bgManager.shutdown();
@@ -919,6 +972,49 @@ export default function(pi: ExtensionAPI) {
919
972
  },
920
973
  });
921
974
 
975
+ pi.registerShortcut(Key.ctrl("b"), {
976
+ description: shortcutDesc("Move foreground subagent to background", "/subagents list"),
977
+ handler: async (ctx) => {
978
+ const running = activeForegroundSubagent;
979
+ if (!running || running.claimed) return;
980
+ const manager = bgManager;
981
+ if (!manager) {
982
+ ctx.ui.notify("Background subagent manager is not available.", "error");
983
+ return;
984
+ }
985
+
986
+ running.claimed = true;
987
+ let jobId: string;
988
+ try {
989
+ jobId = manager.adoptRunning(
990
+ running.agentName,
991
+ running.task,
992
+ running.cwd,
993
+ running.abortController,
994
+ running.resultPromise,
995
+ );
996
+ } catch (error) {
997
+ running.claimed = false;
998
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
999
+ return;
1000
+ }
1001
+
1002
+ const released = running.adoptToBackground(jobId);
1003
+ if (!released) {
1004
+ running.claimed = false;
1005
+ manager.cancel(jobId);
1006
+ return;
1007
+ }
1008
+
1009
+ activeForegroundSubagent = null;
1010
+ ctx.ui.setStatus(foregroundSubagentStatusKey, undefined);
1011
+ ctx.ui.notify(
1012
+ `Moved ${running.agentName} to background as ${jobId}. Use /subagents wait ${jobId}, /subagents output ${jobId}, or /subagents cancel ${jobId}.`,
1013
+ "info",
1014
+ );
1015
+ },
1016
+ });
1017
+
922
1018
  pi.registerTool({
923
1019
  name: "await_subagent",
924
1020
  label: "Await Background Subagent",
@@ -949,13 +1045,15 @@ export default function(pi: ExtensionAPI) {
949
1045
  "Model selection can be overridden per call, otherwise it is inferred from the agent config, delegated-task preferences, or the current session model.",
950
1046
  "Modes: single ({ agent, task }), parallel ({ tasks: [{agent, task},...] }), chain ({ chain: [{agent, task},...] } with {previous} placeholder).",
951
1047
  "Agents are defined as .md files in the configured user agent directory (for LSD this is typically ~/.lsd/agent/agents/) or project-local .lsd/agents/, with legacy support for .gsd/agents/ and .pi/agents/.",
1048
+ "If the user asks for a named subagent such as scout, worker, reviewer, or planner, invoke this tool directly rather than the Skill tool.",
952
1049
  "Use the /subagent command to list available agents and their descriptions.",
953
1050
  "Set background: true (single mode only) to run detached — returns immediately with a sa_xxxx job ID. Completion is announced back into the session. Use await_subagent or /subagents to manage background jobs.",
954
1051
  ].join(" "),
955
1052
  promptGuidelines: [
956
1053
  "Use subagent to delegate self-contained tasks that benefit from an isolated context window.",
957
- "When scout is the right fit, call the subagent tool directly do not type '/subagent' as a shell-like command.",
1054
+ "The subagent tool is available directly as a tool call invoke it programmatically like any other tool, not via a slash command. Do NOT type '/scout' or '/subagent' in the chat; call this tool with the correct parameters instead.",
958
1055
  "Valid call shapes: single mode uses { agent, task }, parallel mode uses { tasks: [{ agent, task }, ...] }, and chain mode uses { chain: [{ agent, task }, ...] }.",
1056
+ "If the user names a subagent such as scout, worker, reviewer, or planner, use this subagent tool directly rather than the Skill tool or ad-hoc search.",
959
1057
  "Recon planning rule: use no scout for narrow known-file work, one scout for one broad unfamiliar subsystem, and parallel scouts only when the work spans multiple loosely-coupled subsystems.",
960
1058
  "Use scout only for broad or unfamiliar codebase reconnaissance before you read many files yourself; save direct reads for targeted lookups once the relevant files are known.",
961
1059
  "Do not use scout as the reviewer, auditor, or final judge. Scout should map architecture, files, ownership, and likely hotspots for another agent or the parent model to evaluate.",
@@ -1301,8 +1399,27 @@ export default function(pi: ExtensionAPI) {
1301
1399
  signal,
1302
1400
  onUpdate,
1303
1401
  makeDetails("single"),
1402
+ !isolation
1403
+ ? {
1404
+ onStart: (control) => {
1405
+ activeForegroundSubagent = { ...control, claimed: false };
1406
+ ctx.ui.setStatus(foregroundSubagentStatusKey, foregroundSubagentHint);
1407
+ },
1408
+ onFinish: () => {
1409
+ activeForegroundSubagent = null;
1410
+ ctx.ui.setStatus(foregroundSubagentStatusKey, undefined);
1411
+ },
1412
+ }
1413
+ : undefined,
1304
1414
  );
1305
1415
 
1416
+ if (result.backgroundJobId) {
1417
+ return {
1418
+ content: [{ type: "text", text: `Moved ${result.agent} to background as **${result.backgroundJobId}**. Use \`await_subagent\`, \`/subagents wait ${result.backgroundJobId}\`, or \`/subagents output ${result.backgroundJobId}\`.` }],
1419
+ details: makeDetails("single")([result]),
1420
+ };
1421
+ }
1422
+
1306
1423
  // Capture and merge delta if isolated
1307
1424
  if (isolation) {
1308
1425
  const patches = await isolation.captureDelta();
@@ -1464,6 +1581,9 @@ export default function(pi: ExtensionAPI) {
1464
1581
  text += `\n${renderDisplayItems(displayItems, COLLAPSED_ITEM_COUNT)}`;
1465
1582
  if (displayItems.length > COLLAPSED_ITEM_COUNT) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
1466
1583
  }
1584
+ if (!isError && !r.backgroundJobId && !finalOutput) {
1585
+ text += `\n${theme.fg("muted", "Hint: Ctrl+B to move running foreground subagent to background")}`;
1586
+ }
1467
1587
  const usageStr = formatUsageStats(r.usage, r.model);
1468
1588
  if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
1469
1589
  return new Text(text, 0, 0);