pi-prompt-template-model 0.9.3 → 0.10.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/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.10.0] - 2026-07-01
6
+
7
+ ### Changed
8
+ - Updated local Pi development dependencies to `@earendil-works/*` `0.80.3` and declared Pi core packages plus `typebox` as peer dependencies for package installs.
9
+
10
+ ### Fixed
11
+ - Inline prompt templates with `restore: true` now restore the previous model and thinking level on the prompt turn's `agent_end`, instead of one unrelated user turn later.
12
+ - Idle `agent_end` events now skip context/model reads when there is no pending restore state or queued `run-prompt` command.
13
+ - Prompt commands now initialize from the active session cwd instead of `process.cwd()`, and extension-owned Pi paths use Pi's current config-directory helpers.
14
+ - Delegated prompt execution now uses the loaded `pi-subagents` event bridge instead of importing `pi-subagents` internals for agent discovery.
15
+ - Prompt templates listed in user or project `settings.json` `prompts` paths now receive this extension's frontmatter handling, including `model`, `thinking`, and `skill`.
16
+
5
17
  ## [0.9.3] - 2026-04-28
6
18
 
7
19
  ### Fixed
package/README.md CHANGED
@@ -49,6 +49,19 @@ Start a Python REPL session and help me debug: $@
49
49
 
50
50
  Run `/debug-python some issue` and the agent switches to Sonnet, receives the tmux skill as context, and starts working. When it finishes, your previous model is restored.
51
51
 
52
+ ## Prompt Discovery
53
+
54
+ The extension scans the default prompt directories and the same local prompt paths Pi loads from `settings.json`:
55
+
56
+ - User defaults: `~/.pi/agent/prompts/`
57
+ - Project defaults: `<cwd>/.pi/prompts/`
58
+ - User settings: `~/.pi/agent/settings.json` `prompts` entries
59
+ - Project settings: `<cwd>/.pi/settings.json` `prompts` entries
60
+
61
+ Settings entries may point at directories or individual `.md` files. Absolute paths and `~/...` work. Relative user settings paths resolve from `~/.pi/agent`; relative project settings paths resolve from `<cwd>/.pi`. Pattern entries follow Pi's settings behavior: glob-style entries filter configured prompt paths, and `!`, `+`, or `-` entries exclude or force exact matches.
62
+
63
+ Precedence follows Pi's local-resource order: project settings, project defaults, user settings, then user defaults. If the same file is reachable through both settings and a default directory, it is loaded once by canonical path. Malformed settings files or invalid configured paths produce warnings while the rest of discovery continues.
64
+
52
65
  ## Frontmatter Reference
53
66
 
54
67
  All fields are optional. Templates that don't use any extension features (no `model`, `skill`, `thinking`, etc.) are left to pi's default prompt loader.
package/chain-parser.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { parseCommandArgs } from "./args.js";
1
+ import { parseCommandArgs } from "./args.ts";
2
2
 
3
3
  export interface ChainStep {
4
4
  name: string;
@@ -1,6 +1,6 @@
1
- import type { MessageRenderOptions, Theme } from "@mariozechner/pi-coding-agent";
2
- import { Box, Container, Spacer, Text } from "@mariozechner/pi-tui";
3
- import { formatDeterministicExecution, type DeterministicExecutionResult } from "./deterministic-step.js";
1
+ import type { MessageRenderOptions, Theme } from "@earendil-works/pi-coding-agent";
2
+ import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
3
+ import { formatDeterministicExecution, type DeterministicExecutionResult } from "./deterministic-step.ts";
4
4
 
5
5
  interface DeterministicMessage {
6
6
  content?: unknown;
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import { dirname, isAbsolute, resolve } from "node:path";
4
- import type { PromptWithModel, DeterministicStep, DeterministicExecution, DeterministicEnv } from "./prompt-loader.js";
4
+ import type { PromptWithModel, DeterministicStep, DeterministicExecution, DeterministicEnv } from "./prompt-loader.ts";
5
5
 
6
6
  export const PROMPT_TEMPLATE_DETERMINISTIC_MESSAGE_TYPE = "prompt-template-deterministic";
7
7
  export const PROMPT_TEMPLATE_DETERMINISTIC_COMPLETION_MESSAGE_TYPE = "prompt-template-deterministic-complete";
package/index.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { resolve as resolvePath } from "node:path";
3
- import type { Model } from "@mariozechner/pi-ai";
4
- import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
5
- import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
3
+ import type { Model } from "@earendil-works/pi-ai";
4
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
5
+ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
6
6
  import {
7
7
  extractChainContextFlag,
8
8
  extractLineupOverrides,
@@ -14,12 +14,12 @@ import {
14
14
  substituteArgs,
15
15
  type LineupOverrideAction,
16
16
  type SubagentOverride,
17
- } from "./args.js";
18
- import { parseChainSteps, parseChainDeclaration, type ChainStep, type ChainStepOrParallel, type ParallelChainStep } from "./chain-parser.js";
19
- import { generateBoomerangSummary, generateChainStepSummary, generateIterationSummary, didIterationMakeChanges, getIterationEntries, wasIterationAborted } from "./loop-utils.js";
20
- import { selectModelCandidate } from "./model-selection.js";
21
- import { notify, summarizePromptDiagnostics, diagnosticsFingerprint } from "./notifications.js";
22
- import { preparePromptExecution, renderPromptForResolvedModel } from "./prompt-execution.js";
17
+ } from "./args.ts";
18
+ import { parseChainSteps, parseChainDeclaration, type ChainStep, type ChainStepOrParallel, type ParallelChainStep } from "./chain-parser.ts";
19
+ import { generateBoomerangSummary, generateChainStepSummary, generateIterationSummary, didIterationMakeChanges, getIterationEntries, wasIterationAborted } from "./loop-utils.ts";
20
+ import { selectModelCandidate } from "./model-selection.ts";
21
+ import { notify, summarizePromptDiagnostics, diagnosticsFingerprint } from "./notifications.ts";
22
+ import { preparePromptExecution, renderPromptForResolvedModel } from "./prompt-execution.ts";
23
23
  import {
24
24
  buildPromptCommandDescription,
25
25
  expandCwdPath,
@@ -28,20 +28,20 @@ import {
28
28
  resolveSkillPath,
29
29
  type DelegationLineupSlot,
30
30
  type PromptWithModel,
31
- } from "./prompt-loader.js";
32
- import { renderSkillLoaded, type SkillLoadedDetails } from "./skill-loaded-renderer.js";
33
- import { createToolManager } from "./tool-manager.js";
34
- import { executeSubagentPromptStep, type DelegatedPromptParallelResult } from "./subagent-step.js";
35
- import { DEFAULT_SUBAGENT_NAME, PROMPT_TEMPLATE_SUBAGENT_MESSAGE_TYPE } from "./subagent-runtime.js";
36
- import { renderDelegatedSubagentResult } from "./subagent-renderer.js";
31
+ } from "./prompt-loader.ts";
32
+ import { renderSkillLoaded, type SkillLoadedDetails } from "./skill-loaded-renderer.ts";
33
+ import { createToolManager } from "./tool-manager.ts";
34
+ import { executeSubagentPromptStep, type DelegatedPromptParallelResult } from "./subagent-step.ts";
35
+ import { DEFAULT_SUBAGENT_NAME, PROMPT_TEMPLATE_SUBAGENT_MESSAGE_TYPE } from "./subagent-runtime.ts";
36
+ import { renderDelegatedSubagentResult } from "./subagent-renderer.ts";
37
37
  import {
38
38
  PROMPT_TEMPLATE_DETERMINISTIC_COMPLETION_MESSAGE_TYPE,
39
39
  PROMPT_TEMPLATE_DETERMINISTIC_MESSAGE_TYPE,
40
40
  buildDeterministicPreamble,
41
41
  runDeterministicStep,
42
42
  shouldHandoffToLlm,
43
- } from "./deterministic-step.js";
44
- import { renderDeterministicCompletion, renderDeterministicResult } from "./deterministic-renderer.js";
43
+ } from "./deterministic-step.ts";
44
+ import { renderDeterministicCompletion, renderDeterministicResult } from "./deterministic-renderer.ts";
45
45
 
46
46
  interface LoopState {
47
47
  currentIteration: number;
@@ -84,6 +84,11 @@ interface PromptStepResult {
84
84
  text?: string;
85
85
  }
86
86
 
87
+ interface PromptTurnRestore {
88
+ originalModel: Model<any> | undefined;
89
+ originalThinking: ThinkingLevel | undefined;
90
+ }
91
+
87
92
  const DEFAULT_COMPARE_REVIEWER_TASK = [
88
93
  "Review the worker variants and produce findings only.",
89
94
  "Required output:",
@@ -264,6 +269,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
264
269
  inheritedModel?: Model<any>,
265
270
  taskPreamble?: string,
266
271
  loopContext?: string,
272
+ promptTurnRestore?: PromptTurnRestore,
267
273
  ): Promise<PromptStepResult | "aborted"> {
268
274
  let deterministicPreamble: string | undefined;
269
275
  if (prompt.deterministic) {
@@ -377,6 +383,17 @@ export default function promptModelExtension(pi: ExtensionAPI) {
377
383
  }
378
384
  pendingSkillMessage = skillResolution.kind === "ready" ? skillResolution.message : undefined;
379
385
 
386
+ if (promptTurnRestore) {
387
+ const currentModel = getCurrentModel(ctx);
388
+ if (promptTurnRestore.originalModel && currentModel && !sameModel(promptTurnRestore.originalModel, currentModel)) {
389
+ previousModel = promptTurnRestore.originalModel;
390
+ previousThinking = promptTurnRestore.originalThinking;
391
+ }
392
+ if (prompt.thinking && previousThinking === undefined && prompt.thinking !== promptTurnRestore.originalThinking) {
393
+ previousThinking = promptTurnRestore.originalThinking;
394
+ }
395
+ }
396
+
380
397
  const startId = ctx.sessionManager.getLeafId();
381
398
  const effectiveContent = combinedTaskPreamble
382
399
  ? `${combinedTaskPreamble}\n\n${prepared.content}`
@@ -1587,6 +1604,10 @@ export default function promptModelExtension(pi: ExtensionAPI) {
1587
1604
  };
1588
1605
  const savedModel = getCurrentModel(ctx);
1589
1606
  const savedThinking = pi.getThinkingLevel();
1607
+ const isDelegatedPrompt = shouldDelegatePrompt(effectivePrompt, subagent.override);
1608
+ const promptTurnRestore = !isDelegatedPrompt && prompt.restore
1609
+ ? { originalModel: savedModel, originalThinking: savedThinking }
1610
+ : undefined;
1590
1611
  const boomerangTargetId = effectivePrompt.boomerang ? ctx.sessionManager.getLeafId() : null;
1591
1612
  const stepResult = await executePromptStep(
1592
1613
  effectivePrompt,
@@ -1594,25 +1615,18 @@ export default function promptModelExtension(pi: ExtensionAPI) {
1594
1615
  ctx,
1595
1616
  savedModel,
1596
1617
  subagent.override,
1618
+ undefined,
1619
+ undefined,
1620
+ undefined,
1621
+ promptTurnRestore,
1597
1622
  );
1598
1623
  if (stepResult === "aborted") return;
1599
- if (shouldDelegatePrompt(effectivePrompt, subagent.override) && stepResult.text) {
1624
+ if (isDelegatedPrompt && stepResult.text) {
1600
1625
  pi.sendUserMessage(`[Delegated result: ${name}]\n\n${stepResult.text}`);
1601
1626
  await waitForTurnStart(ctx);
1602
1627
  await ctx.waitForIdle();
1603
1628
  }
1604
1629
 
1605
- if (!shouldDelegatePrompt(effectivePrompt, subagent.override) && prompt.restore) {
1606
- const currentModel = getCurrentModel(ctx);
1607
- if (savedModel && currentModel && !sameModel(savedModel, currentModel)) {
1608
- previousModel = savedModel;
1609
- previousThinking = savedThinking;
1610
- }
1611
- if (effectivePrompt.thinking && previousThinking === undefined && effectivePrompt.thinking !== savedThinking) {
1612
- previousThinking = savedThinking;
1613
- }
1614
- }
1615
-
1616
1630
  if (effectivePrompt.boomerang) {
1617
1631
  await collapseBoomerangPrompt(ctx, name, boomerangTargetId);
1618
1632
  }
@@ -1670,10 +1684,12 @@ export default function promptModelExtension(pi: ExtensionAPI) {
1670
1684
  if (chainActive) return;
1671
1685
  if (loopState) return;
1672
1686
 
1673
- runtimeModel = ctx.model;
1674
-
1675
1687
  const restoreModel = previousModel;
1676
1688
  const restoreThinking = previousThinking;
1689
+ if (!restoreModel && restoreThinking === undefined && !toolManager.hasQueuedCommand()) return;
1690
+
1691
+ runtimeModel = ctx.model;
1692
+
1677
1693
  previousModel = undefined;
1678
1694
  previousThinking = undefined;
1679
1695
 
@@ -1756,7 +1772,6 @@ export default function promptModelExtension(pi: ExtensionAPI) {
1756
1772
  );
1757
1773
  }
1758
1774
 
1759
- refreshPrompts(process.cwd());
1760
1775
  if (toolManager.isEnabled()) toolManager.ensureRegistered();
1761
1776
 
1762
1777
  pi.registerCommand("chain-prompts", {
package/loop-utils.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { AssistantMessage, Message } from "@mariozechner/pi-ai";
2
- import type { ExtensionContext, SessionEntry } from "@mariozechner/pi-coding-agent";
3
- import { PROMPT_TEMPLATE_SUBAGENT_MESSAGE_TYPE } from "./subagent-runtime.js";
1
+ import type { AssistantMessage, Message } from "@earendil-works/pi-ai";
2
+ import type { ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
3
+ import { PROMPT_TEMPLATE_SUBAGENT_MESSAGE_TYPE } from "./subagent-runtime.ts";
4
4
 
5
5
  interface DelegatedMessageDetails {
6
6
  messages?: Message[];
@@ -1,5 +1,5 @@
1
- import type { Model } from "@mariozechner/pi-ai";
2
- import type { ResolvedModelRef } from "./template-conditionals.js";
1
+ import type { Model } from "@earendil-works/pi-ai";
2
+ import type { ResolvedModelRef } from "./template-conditionals.ts";
3
3
 
4
4
  const PREFERRED_PROVIDERS = ["openai-codex", "anthropic", "github-copilot", "openrouter"];
5
5
 
package/notifications.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
- import type { PromptLoaderDiagnostic } from "./prompt-loader.js";
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { PromptLoaderDiagnostic } from "./prompt-loader.ts";
3
3
 
4
4
  export function notify(
5
5
  ctx: Pick<ExtensionContext, "hasUI" | "ui"> | undefined,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-prompt-template-model",
3
- "version": "0.9.3",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "description": "Prompt template model selector extension for pi coding agent",
6
6
  "author": "Nico Bailon",
@@ -49,15 +49,20 @@
49
49
  "scripts": {
50
50
  "test": "tsx --test test/**/*.test.ts"
51
51
  },
52
- "dependencies": {
53
- "typebox": "^1.1.24"
52
+ "peerDependencies": {
53
+ "@earendil-works/pi-agent-core": "*",
54
+ "@earendil-works/pi-ai": "*",
55
+ "@earendil-works/pi-coding-agent": "*",
56
+ "@earendil-works/pi-tui": "*",
57
+ "typebox": "*"
54
58
  },
55
59
  "devDependencies": {
56
- "@mariozechner/pi-agent-core": "^0.65.0",
57
- "@mariozechner/pi-ai": "^0.65.0",
58
- "@mariozechner/pi-coding-agent": "^0.65.0",
59
- "@mariozechner/pi-tui": "^0.65.0",
60
- "tsx": "^4.20.5"
60
+ "@earendil-works/pi-agent-core": "^0.80.3",
61
+ "@earendil-works/pi-ai": "^0.80.3",
62
+ "@earendil-works/pi-coding-agent": "^0.80.3",
63
+ "@earendil-works/pi-tui": "^0.80.3",
64
+ "tsx": "^4.22.4",
65
+ "typebox": "^1.3.2"
61
66
  },
62
67
  "pi": {
63
68
  "extensions": [
@@ -66,5 +71,8 @@
66
71
  "skills": [
67
72
  "./skills"
68
73
  ]
74
+ },
75
+ "dependencies": {
76
+ "minimatch": "^10.2.5"
69
77
  }
70
78
  }
@@ -1,8 +1,8 @@
1
- import type { Model } from "@mariozechner/pi-ai";
2
- import { substituteArgs } from "./args.js";
3
- import { getResolvedModelRef, selectModelCandidate, type RegistryLike, type SelectedModelCandidate } from "./model-selection.js";
4
- import type { PromptWithModel } from "./prompt-loader.js";
5
- import { renderTemplateConditionals } from "./template-conditionals.js";
1
+ import type { Model } from "@earendil-works/pi-ai";
2
+ import { substituteArgs } from "./args.ts";
3
+ import { getResolvedModelRef, selectModelCandidate, type RegistryLike, type SelectedModelCandidate } from "./model-selection.ts";
4
+ import type { PromptWithModel } from "./prompt-loader.ts";
5
+ import { renderTemplateConditionals } from "./template-conditionals.ts";
6
6
 
7
7
  export interface PreparedPromptExecution {
8
8
  selectedModel: SelectedModelCandidate;
package/prompt-loader.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { dirname, isAbsolute, join, resolve } from "node:path";
4
- import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
5
- import { parseFrontmatter } from "@mariozechner/pi-coding-agent";
6
- import { parseChainDeclaration } from "./chain-parser.js";
3
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
+ import { minimatch } from "minimatch";
5
+ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
6
+ import { CONFIG_DIR_NAME, getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
7
+ import { parseChainDeclaration } from "./chain-parser.ts";
7
8
 
8
9
  const VALID_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
9
10
  export const RESERVED_COMMAND_NAMES = new Set([
@@ -1343,12 +1344,201 @@ function normalizeThinkingLevels(
1343
1344
  return levels.map((level) => level.toLowerCase() as ThinkingLevel);
1344
1345
  }
1345
1346
 
1347
+ interface ConfiguredPromptPath {
1348
+ rawPath: string;
1349
+ resolvedPath: string;
1350
+ settingsPath: string;
1351
+ source: PromptSource;
1352
+ baseDir: string;
1353
+ patterns: string[];
1354
+ }
1355
+
1356
+ interface ConfiguredPromptPaths {
1357
+ paths: ConfiguredPromptPath[];
1358
+ patterns: string[];
1359
+ }
1360
+
1361
+ function expandConfiguredPromptPath(rawPath: string, baseDir: string): string {
1362
+ const trimmed = rawPath.trim();
1363
+ const expanded = trimmed === "~" ? homedir() : trimmed.startsWith("~/") ? join(homedir(), trimmed.slice(2)) : trimmed;
1364
+ return isAbsolute(expanded) ? resolve(expanded) : resolve(baseDir, expanded);
1365
+ }
1366
+
1367
+ function isSettingsPromptPattern(value: string): boolean {
1368
+ return value.startsWith("!") || value.startsWith("+") || value.startsWith("-") || value.includes("*") || value.includes("?");
1369
+ }
1370
+
1371
+ function isSettingsPromptOverridePattern(value: string): boolean {
1372
+ return value.startsWith("!") || value.startsWith("+") || value.startsWith("-");
1373
+ }
1374
+
1375
+ function toPosixPath(path: string): string {
1376
+ return path.replace(/\\/g, "/");
1377
+ }
1378
+
1379
+ function normalizeExactPromptPattern(pattern: string): string {
1380
+ const normalized = pattern.startsWith("./") || pattern.startsWith(".\\") ? pattern.slice(2) : pattern;
1381
+ return toPosixPath(normalized);
1382
+ }
1383
+
1384
+ function matchesPromptPattern(filePath: string, pattern: string, baseDir: string): boolean {
1385
+ const normalizedPattern = toPosixPath(pattern);
1386
+ const rel = toPosixPath(relative(baseDir, filePath));
1387
+ const name = basename(filePath);
1388
+ const filePathPosix = toPosixPath(filePath);
1389
+ return [rel, name, filePathPosix].some((candidate) => minimatch(candidate, normalizedPattern));
1390
+ }
1391
+
1392
+ function matchesExactPromptPattern(filePath: string, pattern: string, baseDir: string): boolean {
1393
+ const normalizedPattern = normalizeExactPromptPattern(pattern);
1394
+ const rel = toPosixPath(relative(baseDir, filePath));
1395
+ const filePathPosix = toPosixPath(filePath);
1396
+ return normalizedPattern === rel || normalizedPattern === filePathPosix;
1397
+ }
1398
+
1399
+ function shouldLoadPromptFile(filePath: string, patterns: string[], baseDir: string): boolean {
1400
+ const includes: string[] = [];
1401
+ const excludes: string[] = [];
1402
+ const forceIncludes: string[] = [];
1403
+ const forceExcludes: string[] = [];
1404
+
1405
+ for (const pattern of patterns) {
1406
+ if (pattern.startsWith("+")) {
1407
+ forceIncludes.push(pattern.slice(1));
1408
+ } else if (pattern.startsWith("-")) {
1409
+ forceExcludes.push(pattern.slice(1));
1410
+ } else if (pattern.startsWith("!")) {
1411
+ excludes.push(pattern.slice(1));
1412
+ } else {
1413
+ includes.push(pattern);
1414
+ }
1415
+ }
1416
+
1417
+ let enabled = includes.length === 0 || includes.some((pattern) => matchesPromptPattern(filePath, pattern, baseDir));
1418
+ if (excludes.some((pattern) => matchesPromptPattern(filePath, pattern, baseDir))) enabled = false;
1419
+ if (forceIncludes.some((pattern) => matchesExactPromptPattern(filePath, pattern, baseDir))) enabled = true;
1420
+ if (forceExcludes.some((pattern) => matchesExactPromptPattern(filePath, pattern, baseDir))) enabled = false;
1421
+ return enabled;
1422
+ }
1423
+
1424
+ function createPromptFileFilter(patterns: string[], baseDir: string): ((filePath: string) => boolean) | undefined {
1425
+ return patterns.length === 0 ? undefined : (filePath) => shouldLoadPromptFile(filePath, patterns, baseDir);
1426
+ }
1427
+
1428
+ function loadConfiguredPromptPaths(
1429
+ settingsPath: string,
1430
+ source: PromptSource,
1431
+ baseDir: string,
1432
+ diagnostics: PromptLoaderDiagnostic[],
1433
+ ): ConfiguredPromptPaths {
1434
+ if (!existsSync(settingsPath)) return { paths: [], patterns: [] };
1435
+
1436
+ let rawSettings: string;
1437
+ try {
1438
+ rawSettings = readFileSync(settingsPath, "utf-8");
1439
+ } catch (error) {
1440
+ diagnostics.push(
1441
+ createDiagnostic(
1442
+ "unreadable-settings",
1443
+ settingsPath,
1444
+ source,
1445
+ `Skipping prompt settings at ${settingsPath}: ${error instanceof Error ? error.message : String(error)}.`,
1446
+ ),
1447
+ );
1448
+ return { paths: [], patterns: [] };
1449
+ }
1450
+
1451
+ let parsed: unknown;
1452
+ try {
1453
+ parsed = JSON.parse(rawSettings);
1454
+ } catch (error) {
1455
+ diagnostics.push(
1456
+ createDiagnostic(
1457
+ "invalid-settings-json",
1458
+ settingsPath,
1459
+ source,
1460
+ `Skipping prompt settings at ${settingsPath}: ${error instanceof Error ? error.message : String(error)}.`,
1461
+ ),
1462
+ );
1463
+ return { paths: [], patterns: [] };
1464
+ }
1465
+
1466
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1467
+ diagnostics.push(
1468
+ createDiagnostic(
1469
+ "invalid-settings",
1470
+ settingsPath,
1471
+ source,
1472
+ `Skipping prompt settings at ${settingsPath}: settings must be a JSON object.`,
1473
+ ),
1474
+ );
1475
+ return { paths: [], patterns: [] };
1476
+ }
1477
+
1478
+ const prompts = (parsed as Record<string, unknown>).prompts;
1479
+ if (prompts === undefined) return { paths: [], patterns: [] };
1480
+ if (!Array.isArray(prompts)) {
1481
+ diagnostics.push(
1482
+ createDiagnostic(
1483
+ "invalid-settings-prompts",
1484
+ settingsPath,
1485
+ source,
1486
+ `Ignoring prompts in ${settingsPath}: expected "prompts" to be an array of strings.`,
1487
+ ),
1488
+ );
1489
+ return { paths: [], patterns: [] };
1490
+ }
1491
+
1492
+ const configuredPaths: Omit<ConfiguredPromptPath, "patterns">[] = [];
1493
+ const patterns: string[] = [];
1494
+ for (let index = 0; index < prompts.length; index++) {
1495
+ const rawPath = prompts[index];
1496
+ if (typeof rawPath !== "string" || rawPath.trim().length === 0) {
1497
+ diagnostics.push(
1498
+ createDiagnostic(
1499
+ "invalid-settings-prompt-entry",
1500
+ settingsPath,
1501
+ source,
1502
+ `Ignoring prompts[${index}] in ${settingsPath}: expected a non-empty string.`,
1503
+ ),
1504
+ );
1505
+ continue;
1506
+ }
1507
+
1508
+ const trimmed = rawPath.trim();
1509
+ if (isSettingsPromptPattern(trimmed)) {
1510
+ patterns.push(trimmed);
1511
+ continue;
1512
+ }
1513
+ configuredPaths.push({
1514
+ rawPath: trimmed,
1515
+ resolvedPath: expandConfiguredPromptPath(trimmed, baseDir),
1516
+ settingsPath,
1517
+ source,
1518
+ baseDir,
1519
+ });
1520
+ }
1521
+
1522
+ return { paths: configuredPaths.map((path) => ({ ...path, patterns })), patterns };
1523
+ }
1524
+
1525
+ function canonicalizePromptFile(filePath: string): string {
1526
+ try {
1527
+ return realpathSync(filePath);
1528
+ } catch {
1529
+ return resolve(filePath);
1530
+ }
1531
+ }
1532
+
1346
1533
  function loadPromptsWithModelFromDir(
1347
1534
  dir: string,
1348
1535
  source: PromptSource,
1349
1536
  includePlainPrompts: boolean,
1350
1537
  subdir = "",
1351
1538
  visitedDirectories = new Set<string>(),
1539
+ onlyFileName?: string,
1540
+ seenFiles?: Set<string>,
1541
+ shouldLoadFile?: (filePath: string) => boolean,
1352
1542
  ): { prompts: PromptWithModel[]; diagnostics: PromptLoaderDiagnostic[] } {
1353
1543
  const prompts: PromptWithModel[] = [];
1354
1544
  const diagnostics: PromptLoaderDiagnostic[] = [];
@@ -1390,6 +1580,7 @@ function loadPromptsWithModelFromDir(
1390
1580
  const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => lexicalCompare(a.name, b.name));
1391
1581
 
1392
1582
  for (const entry of entries) {
1583
+ if (onlyFileName && entry.name !== onlyFileName) continue;
1393
1584
  const fullPath = join(dir, entry.name);
1394
1585
 
1395
1586
  let isFile = entry.isFile();
@@ -1413,14 +1604,19 @@ function loadPromptsWithModelFromDir(
1413
1604
  }
1414
1605
 
1415
1606
  if (isDirectory) {
1607
+ if (onlyFileName) continue;
1416
1608
  const nextSubdir = subdir ? `${subdir}:${entry.name}` : entry.name;
1417
- const nested = loadPromptsWithModelFromDir(fullPath, source, includePlainPrompts, nextSubdir, visitedDirectories);
1609
+ const nested = loadPromptsWithModelFromDir(fullPath, source, includePlainPrompts, nextSubdir, visitedDirectories, undefined, seenFiles, shouldLoadFile);
1418
1610
  prompts.push(...nested.prompts);
1419
1611
  diagnostics.push(...nested.diagnostics);
1420
1612
  continue;
1421
1613
  }
1422
1614
 
1423
1615
  if (!isFile || !entry.name.endsWith(".md")) continue;
1616
+ if (shouldLoadFile && !shouldLoadFile(fullPath)) continue;
1617
+ const canonicalFile = canonicalizePromptFile(fullPath);
1618
+ if (seenFiles?.has(canonicalFile)) continue;
1619
+ seenFiles?.add(canonicalFile);
1424
1620
 
1425
1621
  try {
1426
1622
  const rawContent = readFileSync(fullPath, "utf-8");
@@ -1802,11 +1998,69 @@ function loadPromptsWithModelFromDir(
1802
1998
  return { prompts, diagnostics };
1803
1999
  }
1804
2000
 
2001
+ function loadPromptsWithModelFromConfiguredPath(
2002
+ configuredPath: ConfiguredPromptPath,
2003
+ includePlainPrompts: boolean,
2004
+ seenFiles: Set<string>,
2005
+ ): { prompts: PromptWithModel[]; diagnostics: PromptLoaderDiagnostic[] } {
2006
+ const diagnostics: PromptLoaderDiagnostic[] = [];
2007
+ const { rawPath, resolvedPath, settingsPath, source, baseDir, patterns } = configuredPath;
2008
+ const shouldLoadFile = createPromptFileFilter(patterns, baseDir);
2009
+
2010
+ if (!existsSync(resolvedPath)) {
2011
+ diagnostics.push(
2012
+ createDiagnostic(
2013
+ "missing-prompt-path",
2014
+ resolvedPath,
2015
+ source,
2016
+ `Skipping configured prompt path ${JSON.stringify(rawPath)} from ${settingsPath}: resolved path ${resolvedPath} does not exist.`,
2017
+ ),
2018
+ );
2019
+ return { prompts: [], diagnostics };
2020
+ }
2021
+
2022
+ let stats: ReturnType<typeof statSync>;
2023
+ try {
2024
+ stats = statSync(resolvedPath);
2025
+ } catch (error) {
2026
+ diagnostics.push(
2027
+ createDiagnostic(
2028
+ "unreadable-prompt-path",
2029
+ resolvedPath,
2030
+ source,
2031
+ `Skipping configured prompt path ${JSON.stringify(rawPath)} from ${settingsPath}: ${error instanceof Error ? error.message : String(error)}.`,
2032
+ ),
2033
+ );
2034
+ return { prompts: [], diagnostics };
2035
+ }
2036
+
2037
+ if (stats.isDirectory()) {
2038
+ return loadPromptsWithModelFromDir(resolvedPath, source, includePlainPrompts, "", new Set<string>(), undefined, seenFiles, shouldLoadFile);
2039
+ }
2040
+
2041
+ if (!stats.isFile() || !resolvedPath.endsWith(".md")) {
2042
+ diagnostics.push(
2043
+ createDiagnostic(
2044
+ "invalid-prompt-path",
2045
+ resolvedPath,
2046
+ source,
2047
+ `Skipping configured prompt path ${JSON.stringify(rawPath)} from ${settingsPath}: expected a directory or .md file.`,
2048
+ ),
2049
+ );
2050
+ return { prompts: [], diagnostics };
2051
+ }
2052
+
2053
+ return loadPromptsWithModelFromDir(dirname(resolvedPath), source, includePlainPrompts, "", new Set<string>(), basename(resolvedPath), seenFiles, shouldLoadFile);
2054
+ }
2055
+
1805
2056
  export function loadPromptsWithModel(cwd: string, includePlainPrompts = false): LoadPromptsWithModelResult {
1806
- const globalDir = join(homedir(), ".pi", "agent", "prompts");
1807
- const projectDir = resolve(cwd, ".pi", "prompts");
2057
+ const agentDir = getAgentDir();
2058
+ const globalDir = join(agentDir, "prompts");
2059
+ const projectBaseDir = resolve(cwd, CONFIG_DIR_NAME);
2060
+ const projectDir = join(projectBaseDir, "prompts");
1808
2061
  const promptMap = new Map<string, PromptWithModel>();
1809
2062
  const diagnostics: PromptLoaderDiagnostic[] = [];
2063
+ const seenPromptFiles = new Set<string>();
1810
2064
 
1811
2065
  function addPrompt(prompt: PromptWithModel) {
1812
2066
  const existing = promptMap.get(prompt.name);
@@ -1824,23 +2078,29 @@ export function loadPromptsWithModel(cwd: string, includePlainPrompts = false):
1824
2078
  `Skipping ${prompt.source} prompt template "${prompt.name}" at ${prompt.filePath} because it conflicts with ${existing.filePath}.`,
1825
2079
  ),
1826
2080
  );
1827
- return;
1828
2081
  }
1829
-
1830
- promptMap.set(prompt.name, prompt);
1831
2082
  }
1832
2083
 
1833
- const globalResult = loadPromptsWithModelFromDir(globalDir, "user", includePlainPrompts);
1834
- diagnostics.push(...globalResult.diagnostics);
1835
- for (const prompt of globalResult.prompts) {
1836
- addPrompt(prompt);
2084
+ function addResult(result: { prompts: PromptWithModel[]; diagnostics: PromptLoaderDiagnostic[] }) {
2085
+ diagnostics.push(...result.diagnostics);
2086
+ for (const prompt of result.prompts) {
2087
+ addPrompt(prompt);
2088
+ }
1837
2089
  }
1838
2090
 
1839
- const projectResult = loadPromptsWithModelFromDir(projectDir, "project", includePlainPrompts);
1840
- diagnostics.push(...projectResult.diagnostics);
1841
- for (const prompt of projectResult.prompts) {
1842
- addPrompt(prompt);
2091
+ const projectSettingsPaths = loadConfiguredPromptPaths(join(projectBaseDir, "settings.json"), "project", projectBaseDir, diagnostics);
2092
+ const globalSettingsPaths = loadConfiguredPromptPaths(join(agentDir, "settings.json"), "user", agentDir, diagnostics);
2093
+ const projectDefaultFilter = createPromptFileFilter(projectSettingsPaths.patterns.filter(isSettingsPromptOverridePattern), projectBaseDir);
2094
+ const globalDefaultFilter = createPromptFileFilter(globalSettingsPaths.patterns.filter(isSettingsPromptOverridePattern), agentDir);
2095
+
2096
+ for (const configuredPath of projectSettingsPaths.paths) {
2097
+ addResult(loadPromptsWithModelFromConfiguredPath(configuredPath, includePlainPrompts, seenPromptFiles));
2098
+ }
2099
+ addResult(loadPromptsWithModelFromDir(projectDir, "project", includePlainPrompts, "", new Set<string>(), undefined, seenPromptFiles, projectDefaultFilter));
2100
+ for (const configuredPath of globalSettingsPaths.paths) {
2101
+ addResult(loadPromptsWithModelFromConfiguredPath(configuredPath, includePlainPrompts, seenPromptFiles));
1843
2102
  }
2103
+ addResult(loadPromptsWithModelFromDir(globalDir, "user", includePlainPrompts, "", new Set<string>(), undefined, seenPromptFiles, globalDefaultFilter));
1844
2104
 
1845
2105
  return { prompts: promptMap, diagnostics };
1846
2106
  }
@@ -1911,7 +2171,7 @@ function findFirstExisting(paths: string[]): string | undefined {
1911
2171
  export function resolveSkillPath(skillName: string, cwd: string): string | undefined {
1912
2172
  const projectDir = resolve(cwd);
1913
2173
 
1914
- const projectPiSkill = findFirstExisting(getSkillCandidates(resolve(projectDir, ".pi", "skills"), skillName));
2174
+ const projectPiSkill = findFirstExisting(getSkillCandidates(resolve(projectDir, CONFIG_DIR_NAME, "skills"), skillName));
1915
2175
  if (projectPiSkill) return projectPiSkill;
1916
2176
 
1917
2177
  const repoRoot = findRepoRoot(projectDir);
@@ -1920,7 +2180,7 @@ export function resolveSkillPath(skillName: string, cwd: string): string | undef
1920
2180
  if (projectAgentsSkill) return projectAgentsSkill;
1921
2181
  }
1922
2182
 
1923
- const globalPiSkill = findFirstExisting(getSkillCandidates(join(homedir(), ".pi", "agent", "skills"), skillName));
2183
+ const globalPiSkill = findFirstExisting(getSkillCandidates(join(getAgentDir(), "skills"), skillName));
1924
2184
  if (globalPiSkill) return globalPiSkill;
1925
2185
 
1926
2186
  return findFirstExisting(getSkillCandidates(join(homedir(), ".agents", "skills"), skillName));
@@ -1,5 +1,5 @@
1
- import type { MessageRenderOptions, Theme } from "@mariozechner/pi-coding-agent";
2
- import { Box, Container, Spacer, Text } from "@mariozechner/pi-tui";
1
+ import type { MessageRenderOptions, Theme } from "@earendil-works/pi-coding-agent";
2
+ import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
3
3
 
4
4
  export interface SkillLoadedDetails {
5
5
  skillName: string;
@@ -1,5 +1,5 @@
1
- import type { MessageRenderOptions, Theme } from "@mariozechner/pi-coding-agent";
2
- import { Box, Container, Spacer, Text } from "@mariozechner/pi-tui";
1
+ import type { MessageRenderOptions, Theme } from "@earendil-works/pi-coding-agent";
2
+ import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
3
3
 
4
4
  interface AssistantContent {
5
5
  type: string;
@@ -1,7 +1,3 @@
1
- import { existsSync } from "node:fs";
2
- import { dirname, join, resolve } from "node:path";
3
- import { fileURLToPath, pathToFileURL } from "node:url";
4
-
5
1
  export const PROMPT_TEMPLATE_SUBAGENT_REQUEST_EVENT = "prompt-template:subagent:request";
6
2
  export const PROMPT_TEMPLATE_SUBAGENT_STARTED_EVENT = "prompt-template:subagent:started";
7
3
  export const PROMPT_TEMPLATE_SUBAGENT_RESPONSE_EVENT = "prompt-template:subagent:response";
@@ -93,67 +89,8 @@ export interface DelegatedSubagentLiveState {
93
89
  updatedAt: number;
94
90
  }
95
91
 
96
- interface RuntimeAgent {
97
- name: string;
98
- }
99
-
100
- interface DiscoverAgentsResult {
101
- agents: RuntimeAgent[];
102
- }
103
-
104
- type DiscoverAgentsFn = (cwd: string, scope: "user" | "project" | "both") => DiscoverAgentsResult;
105
-
106
- export interface SubagentRuntime {
107
- root: string;
108
- discoverAgents: DiscoverAgentsFn;
109
- }
110
-
111
- let runtimeCache: SubagentRuntime | null = null;
112
92
  const delegatedLiveState = new Map<string, DelegatedSubagentLiveState>();
113
93
 
114
- function runtimeCandidates(cwd: string): string[] {
115
- const fromEnv = process.env.PI_SUBAGENT_RUNTIME_ROOT?.trim();
116
- if (fromEnv) return [resolve(fromEnv)];
117
- const localSibling = resolve(dirname(fileURLToPath(import.meta.url)), "..", "pi-subagents");
118
- return [
119
- resolve(cwd, ".pi", "npm", "node_modules", "pi-subagents"),
120
- localSibling,
121
- ];
122
- }
123
-
124
- function findSubagentRoot(cwd: string): string | undefined {
125
- for (const candidate of runtimeCandidates(cwd)) {
126
- if (existsSync(join(candidate, "agents.ts")) || existsSync(join(candidate, "agents.js"))) {
127
- return candidate;
128
- }
129
- }
130
- return undefined;
131
- }
132
-
133
- async function importRuntimeModule(root: string, baseName: string): Promise<unknown> {
134
- const candidates = [
135
- join(root, `${baseName}.ts`),
136
- join(root, `${baseName}.mts`),
137
- join(root, `${baseName}.js`),
138
- join(root, `${baseName}.mjs`),
139
- ];
140
-
141
- let lastError: unknown;
142
- for (const filePath of candidates) {
143
- if (!existsSync(filePath)) continue;
144
- try {
145
- return await import(pathToFileURL(filePath).href);
146
- } catch (error) {
147
- lastError = error;
148
- }
149
- }
150
-
151
- if (lastError !== undefined) {
152
- throw lastError;
153
- }
154
- throw new Error(`Missing runtime module: ${baseName}`);
155
- }
156
-
157
94
  export function updateDelegatedLiveState(requestId: string, update: Partial<DelegatedSubagentLiveState>): void {
158
95
  const now = Date.now();
159
96
  const existing = delegatedLiveState.get(requestId) ?? {
@@ -210,38 +147,3 @@ export function getDelegatedLiveState(requestId: string): DelegatedSubagentLiveS
210
147
  export function clearDelegatedLiveState(requestId: string): void {
211
148
  delegatedLiveState.delete(requestId);
212
149
  }
213
-
214
- export async function ensureSubagentRuntime(cwd: string): Promise<SubagentRuntime> {
215
- const root = findSubagentRoot(cwd);
216
- if (!root) {
217
- throw new Error(
218
- "Delegated prompt execution requires pi-subagents. Install it with `pi install npm:pi-subagents` or set PI_SUBAGENT_RUNTIME_ROOT.",
219
- );
220
- }
221
-
222
- if (runtimeCache && runtimeCache.root === root) {
223
- return runtimeCache;
224
- }
225
-
226
- const module = await importRuntimeModule(root, "agents");
227
- const discoverAgents = (module as { discoverAgents?: unknown }).discoverAgents;
228
- if (typeof discoverAgents !== "function") {
229
- throw new Error(`Invalid subagent runtime at ${root}: expected discoverAgents(cwd, scope).`);
230
- }
231
-
232
- runtimeCache = {
233
- root,
234
- discoverAgents: discoverAgents as DiscoverAgentsFn,
235
- };
236
- return runtimeCache;
237
- }
238
-
239
- export function resolveDelegatedAgent(runtime: SubagentRuntime, cwd: string, requested: string): string {
240
- const discovered = runtime.discoverAgents(cwd, "both");
241
- if (!discovered.agents.some((agent) => agent.name === requested)) {
242
- throw new Error(
243
- `Delegated subagent \`${requested}\` not found. Available agents: ${discovered.agents.map((a) => a.name).join(", ") || "none"}.`,
244
- );
245
- }
246
- return requested;
247
- }
package/subagent-step.ts CHANGED
@@ -1,17 +1,16 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { randomUUID } from "node:crypto";
3
- import type { AssistantMessage, Message } from "@mariozechner/pi-ai";
4
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
5
- import type { Model } from "@mariozechner/pi-ai";
6
- import { Key, matchesKey } from "@mariozechner/pi-tui";
7
- import { preparePromptExecution } from "./prompt-execution.js";
8
- import type { PromptWithModel } from "./prompt-loader.js";
9
- import { notify } from "./notifications.js";
3
+ import type { AssistantMessage, Message } from "@earendil-works/pi-ai";
4
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
5
+ import type { Model } from "@earendil-works/pi-ai";
6
+ import { Key, matchesKey } from "@earendil-works/pi-tui";
7
+ import { preparePromptExecution } from "./prompt-execution.ts";
8
+ import type { PromptWithModel } from "./prompt-loader.ts";
9
+ import { notify } from "./notifications.ts";
10
10
  import {
11
11
  DEFAULT_SUBAGENT_NAME,
12
12
  appendDelegatedLiveOutput,
13
13
  clearDelegatedLiveState,
14
- ensureSubagentRuntime,
15
14
  getDelegatedLiveState,
16
15
  PROMPT_TEMPLATE_SUBAGENT_CANCEL_EVENT,
17
16
  PROMPT_TEMPLATE_SUBAGENT_MESSAGE_TYPE,
@@ -19,7 +18,6 @@ import {
19
18
  PROMPT_TEMPLATE_SUBAGENT_RESPONSE_EVENT,
20
19
  PROMPT_TEMPLATE_SUBAGENT_STARTED_EVENT,
21
20
  PROMPT_TEMPLATE_SUBAGENT_UPDATE_EVENT,
22
- resolveDelegatedAgent,
23
21
  updateDelegatedLiveState,
24
22
  type DelegatedSubagentParallelResult,
25
23
  type DelegatedSubagentRequest,
@@ -27,9 +25,9 @@ import {
27
25
  type DelegatedSubagentTask,
28
26
  type DelegatedSubagentTaskProgress,
29
27
  type DelegatedSubagentUpdate,
30
- } from "./subagent-runtime.js";
31
- import type { SubagentOverride } from "./args.js";
32
- import { createDelegatedProgressWidget, DELEGATED_WIDGET_KEY } from "./subagent-widget.js";
28
+ } from "./subagent-runtime.ts";
29
+ import type { SubagentOverride } from "./args.ts";
30
+ import { createDelegatedProgressWidget, DELEGATED_WIDGET_KEY } from "./subagent-widget.ts";
33
31
 
34
32
  interface DelegatedPromptBaseOptions {
35
33
  pi: ExtensionAPI;
@@ -169,7 +167,6 @@ async function prepareDelegatedTask(
169
167
  override: SubagentOverride | undefined,
170
168
  inheritedModel: Model<any> | undefined,
171
169
  taskPreamble: string | undefined,
172
- runtime: Awaited<ReturnType<typeof ensureSubagentRuntime>>,
173
170
  ): Promise<PreparedDelegatedTask> {
174
171
  const requestedAgent = resolveDelegationName(task.prompt, override);
175
172
  if (!requestedAgent) {
@@ -179,7 +176,7 @@ async function prepareDelegatedTask(
179
176
  if (effectiveCwd !== ctx.cwd && !existsSync(effectiveCwd)) {
180
177
  throw new Error(`cwd directory does not exist: ${effectiveCwd}`);
181
178
  }
182
- const agent = resolveDelegatedAgent(runtime, effectiveCwd, requestedAgent);
179
+ const agent = requestedAgent;
183
180
  const preparationOptions = inheritedModel === undefined ? undefined : { inheritedModel };
184
181
  const prepared = await preparePromptExecution(
185
182
  task.prompt,
@@ -340,7 +337,7 @@ async function requestDelegatedRun(
340
337
  const startTimeoutMs = Number(process.env.PI_PROMPT_SUBAGENT_START_TIMEOUT_MS ?? "15000");
341
338
  const effectiveTimeout = Number.isFinite(startTimeoutMs) && startTimeoutMs > 0 ? startTimeoutMs : 15_000;
342
339
  const startTimeout = setTimeout(() => {
343
- finish(() => reject(new Error(`Delegated subagent \`${requestLabel}\` did not start within ${Math.round(effectiveTimeout / 1000)}s. Check that the subagent extension is loaded.`)));
340
+ finish(() => reject(new Error(`Delegated subagent \`${requestLabel}\` did not start within ${Math.round(effectiveTimeout / 1000)}s. Check that the pi-subagents extension is loaded.`)));
344
341
  }, effectiveTimeout);
345
342
 
346
343
  const onStarted = (data: unknown) => {
@@ -528,8 +525,8 @@ async function requestDelegatedRun(
528
525
  if (!started && done) return; // already finished (e.g. response came synchronously)
529
526
  if (!started) {
530
527
  finish(() => reject(new Error(
531
- `No subagent runtime responded for \`${requestLabel}\`. ` +
532
- `Ensure the subagent extension is loaded and has no name conflicts with other extensions.`,
528
+ `No loaded pi-subagents bridge responded for \`${requestLabel}\`. ` +
529
+ `Install or load pi-subagents and make sure no extension name conflicts are blocking it.`,
533
530
  )));
534
531
  return;
535
532
  }
@@ -538,7 +535,6 @@ async function requestDelegatedRun(
538
535
 
539
536
  export async function executeSubagentPromptStep(options: DelegatedPromptOptions): Promise<DelegatedPromptOutcome | undefined> {
540
537
  const { pi, ctx, currentModel, override, signal, inheritedModel, taskPreamble, allowPartialFailures } = options;
541
- const runtime = await ensureSubagentRuntime(ctx.cwd);
542
538
  const isParallelRequest = "parallel" in options;
543
539
 
544
540
  const tasks = isParallelRequest
@@ -548,7 +544,7 @@ export async function executeSubagentPromptStep(options: DelegatedPromptOptions)
548
544
 
549
545
  const preparedTasks: PreparedDelegatedTask[] = [];
550
546
  for (const task of tasks) {
551
- const preparedTask = await prepareDelegatedTask(task, ctx, currentModel, override, inheritedModel, taskPreamble, runtime);
547
+ const preparedTask = await prepareDelegatedTask(task, ctx, currentModel, override, inheritedModel, taskPreamble);
552
548
  preparedTasks.push(preparedTask);
553
549
  }
554
550
 
@@ -1,11 +1,11 @@
1
- import type { Theme } from "@mariozechner/pi-coding-agent";
2
- import { Box, Container, Spacer, Text } from "@mariozechner/pi-tui";
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
3
3
  import {
4
4
  getDelegatedLiveState,
5
5
  type DelegatedSubagentLiveState,
6
6
  type DelegatedSubagentTask,
7
7
  type DelegatedSubagentTaskProgress,
8
- } from "./subagent-runtime.js";
8
+ } from "./subagent-runtime.ts";
9
9
 
10
10
  export const DELEGATED_WIDGET_KEY = "prompt-subagent-progress";
11
11
 
package/tool-manager.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
2
  import { join } from "node:path";
4
- import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
3
+ import { getAgentDir, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext } from "@earendil-works/pi-coding-agent";
5
4
  import { Type } from "typebox";
6
- import { notify } from "./notifications.js";
5
+ import { notify } from "./notifications.ts";
7
6
 
8
7
  export interface ToolManagerDeps {
9
8
  isActive(): boolean;
@@ -17,7 +16,7 @@ export function createToolManager(pi: ExtensionAPI, deps: ToolManagerDeps) {
17
16
  let toolGuidance: string | null = null;
18
17
  let toolRegistered = false;
19
18
  let toolQueuedCommand: string | null = null;
20
- const configPath = join(homedir(), ".pi", "agent", "prompt-template-model.json");
19
+ const configPath = join(getAgentDir(), "prompt-template-model.json");
21
20
 
22
21
  try {
23
22
  const rawConfig = JSON.parse(readFileSync(configPath, "utf-8"));
@@ -41,7 +40,7 @@ export function createToolManager(pi: ExtensionAPI, deps: ToolManagerDeps) {
41
40
 
42
41
  function saveToolConfig() {
43
42
  try {
44
- mkdirSync(join(homedir(), ".pi", "agent"), { recursive: true });
43
+ mkdirSync(getAgentDir(), { recursive: true });
45
44
  writeFileSync(configPath, JSON.stringify({ toolEnabled, toolGuidance }, null, 2));
46
45
  } catch (error) {
47
46
  process.stderr.write(
@@ -209,6 +208,9 @@ export function createToolManager(pi: ExtensionAPI, deps: ToolManagerDeps) {
209
208
  getGuidance() {
210
209
  return toolGuidance;
211
210
  },
211
+ hasQueuedCommand() {
212
+ return toolQueuedCommand !== null;
213
+ },
212
214
  clearQueue() {
213
215
  toolQueuedCommand = null;
214
216
  },