@tintinweb/pi-subagents 0.7.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +22 -1
  3. package/dist/agent-manager.d.ts +2 -2
  4. package/dist/agent-runner.d.ts +3 -3
  5. package/dist/agent-runner.js +1 -1
  6. package/dist/context.d.ts +1 -1
  7. package/dist/custom-agents.js +1 -1
  8. package/dist/default-agents.js +3 -3
  9. package/dist/enabled-models.d.ts +49 -0
  10. package/dist/enabled-models.js +145 -0
  11. package/dist/env.d.ts +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.js +215 -84
  14. package/dist/output-file.d.ts +1 -1
  15. package/dist/schedule-store.d.ts +2 -0
  16. package/dist/schedule-store.js +12 -1
  17. package/dist/schedule.d.ts +1 -1
  18. package/dist/settings.d.ts +23 -0
  19. package/dist/settings.js +6 -1
  20. package/dist/skill-loader.js +1 -1
  21. package/dist/types.d.ts +2 -2
  22. package/dist/ui/agent-widget.js +1 -1
  23. package/dist/ui/conversation-viewer.d.ts +2 -2
  24. package/dist/ui/conversation-viewer.js +1 -1
  25. package/dist/ui/schedule-menu.d.ts +1 -1
  26. package/package.json +4 -4
  27. package/src/agent-manager.ts +2 -2
  28. package/src/agent-runner.ts +3 -3
  29. package/src/context.ts +1 -1
  30. package/src/custom-agents.ts +1 -1
  31. package/src/default-agents.ts +3 -3
  32. package/src/enabled-models.ts +180 -0
  33. package/src/env.ts +1 -1
  34. package/src/index.ts +238 -85
  35. package/src/output-file.ts +1 -1
  36. package/src/schedule-store.ts +11 -1
  37. package/src/schedule.ts +1 -1
  38. package/src/settings.ts +28 -1
  39. package/src/skill-loader.ts +1 -1
  40. package/src/types.ts +2 -2
  41. package/src/ui/agent-widget.ts +1 -1
  42. package/src/ui/conversation-viewer.ts +2 -2
  43. package/src/ui/schedule-menu.ts +1 -1
package/src/index.ts CHANGED
@@ -12,14 +12,15 @@
12
12
 
13
13
  import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
14
14
  import { join } from "node:path";
15
- import { defineTool, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, getAgentDir } from "@mariozechner/pi-coding-agent";
16
- import { Text } from "@mariozechner/pi-tui";
15
+ import { defineTool, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, getAgentDir, getSettingsListTheme } from "@earendil-works/pi-coding-agent";
16
+ import { Container, Key, matchesKey, type SettingItem, SettingsList, Spacer, Text } from "@earendil-works/pi-tui";
17
17
  import { Type } from "@sinclair/typebox";
18
18
  import { AgentManager } from "./agent-manager.js";
19
19
  import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, normalizeMaxTurns, setDefaultMaxTurns, setGraceTurns, steerAgent } from "./agent-runner.js";
20
20
  import { BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, getDefaultAgentNames, getUserAgentNames, registerAgents, resolveType } from "./agent-types.js";
21
21
  import { registerRpcHandlers } from "./cross-extension-rpc.js";
22
22
  import { loadCustomAgents } from "./custom-agents.js";
23
+ import { isModelInScope, readEnabledModels, resolveEnabledModels } from "./enabled-models.js";
23
24
  import { GroupJoinManager } from "./group-join.js";
24
25
  import { resolveAgentInvocationConfig, resolveJoinMode } from "./invocation-config.js";
25
26
  import { type ModelRegistry, resolveModel } from "./model-resolver.js";
@@ -518,6 +519,17 @@ export default function (pi: ExtensionAPI) {
518
519
  function isSchedulingEnabled(): boolean { return schedulingEnabled; }
519
520
  function setSchedulingEnabled(b: boolean) { schedulingEnabled = b; }
520
521
 
522
+ // ---- Scope models configuration ----
523
+ // When enabled, subagent model choices are validated against `enabledModels`
524
+ // from pi's settings — both global `<agentDir>/settings.json` and
525
+ // project-local `<cwd>/.pi/settings.json` (project overrides global).
526
+ // Off by default; opt-in via `/agents → Settings`. See docstring on
527
+ // SubagentsSettings.scopeModels for the hard-error vs warn-and-proceed
528
+ // policy and its rationale.
529
+ let scopeModelsEnabled = false;
530
+ function isScopeModelsEnabled(): boolean { return scopeModelsEnabled; }
531
+ function setScopeModelsEnabled(enabled: boolean): void { scopeModelsEnabled = enabled; }
532
+
521
533
  // ---- Batch tracking for smart join mode ----
522
534
  // Collects background agent IDs spawned in the current turn for smart grouping.
523
535
  // Uses a debounced timer: each new agent resets the 100ms window so that all
@@ -567,29 +579,26 @@ export default function (pi: ExtensionAPI) {
567
579
  widget.onTurnStart();
568
580
  });
569
581
 
582
+ /** Format an agent's tool scope: "*" when it has all built-ins, else a comma-separated list. */
583
+ const formatToolsSuffix = (cfg: AgentConfig | undefined): string => {
584
+ const tools = cfg?.builtinToolNames;
585
+ if (!tools || tools.length === 0) return "*";
586
+ const isFullSet =
587
+ tools.length === BUILTIN_TOOL_NAMES.length
588
+ && BUILTIN_TOOL_NAMES.every((t) => tools.includes(t));
589
+ return isFullSet ? "*" : tools.join(", ");
590
+ };
591
+
570
592
  /** Build the full type list text dynamically from the unified registry. */
571
593
  const buildTypeListText = () => {
572
- const defaultNames = getDefaultAgentNames();
573
- const userNames = getUserAgentNames();
594
+ const allNames = [...getDefaultAgentNames(), ...getUserAgentNames()];
574
595
 
575
- const defaultDescs = defaultNames.map((name) => {
596
+ return allNames.map((name) => {
576
597
  const cfg = getAgentConfig(name);
577
598
  const modelSuffix = cfg?.model ? ` (${getModelLabelFromConfig(cfg.model)})` : "";
578
- return `- ${name}: ${cfg?.description ?? name}${modelSuffix}`;
579
- });
580
-
581
- const customDescs = userNames.map((name) => {
582
- const cfg = getAgentConfig(name);
583
- return `- ${name}: ${cfg?.description ?? name}`;
584
- });
585
-
586
- return [
587
- "Default agents:",
588
- ...defaultDescs,
589
- ...(customDescs.length > 0 ? ["", "Custom agents:", ...customDescs] : []),
590
- "",
591
- `Custom agents can be defined in .pi/agents/<name>.md (project) or ${getAgentDir()}/agents/<name>.md (global) — they are picked up automatically. Project-level agents override global ones. Creating a .md file with the same name as a default agent overrides it.`,
592
- ].join("\n");
599
+ const toolsSuffix = ` (Tools: ${formatToolsSuffix(cfg)})`;
600
+ return `- ${name}: ${cfg?.description ?? name}${modelSuffix}${toolsSuffix}`;
601
+ }).join("\n");
593
602
  };
594
603
 
595
604
  /** Derive a short model label from a model string. */
@@ -612,6 +621,7 @@ export default function (pi: ExtensionAPI) {
612
621
  setGraceTurns,
613
622
  setDefaultJoinMode,
614
623
  setSchedulingEnabled,
624
+ setScopeModels: setScopeModelsEnabled,
615
625
  },
616
626
  (event, payload) => pi.events.emit(event, payload),
617
627
  );
@@ -643,27 +653,48 @@ export default function (pi: ExtensionAPI) {
643
653
  pi.registerTool(defineTool({
644
654
  name: "Agent",
645
655
  label: "Agent",
646
- description: `Launch a new agent to handle complex, multi-step tasks autonomously.
647
-
648
- The Agent tool launches specialized agents that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.
656
+ description: `Launch a new agent to handle complex, multi-step tasks autonomously. Each agent type has specific capabilities and tools available to it.
649
657
 
650
- Available agent types:
658
+ Available agent types and the tools they have access to:
651
659
  ${typeListText}
652
660
 
653
- Guidelines:
654
- - For parallel work, use run_in_background: true on each agent. Foreground calls run sequentially — only one executes at a time.
655
- - Use Explore for codebase searches and code understanding.
656
- - Use Plan for architecture and implementation planning.
657
- - Use general-purpose for complex tasks that need file editing.
658
- - Provide clear, detailed prompts so the agent can work autonomously.
659
- - Agent results are returned as textsummarize them for the user.
660
- - Use run_in_background for work you don't need immediately. You will be notified when it completes.
661
- - Use resume with an agent ID to continue a previous agent's work.
661
+ Custom agents can be defined in .pi/agents/<name>.md (project) or ${getAgentDir()}/agents/<name>.md (global) — they are picked up automatically. Project-level agents override global ones. Creating a .md file with the same name as a default agent overrides it.
662
+
663
+ When using the Agent tool, specify a subagent_type parameter to select which agent type to use.
664
+
665
+ ## When not to use
666
+
667
+ If the target is already known, use a direct tool \`read\` for a known path, \`grep\`/\`find\` for a specific symbol or string. Reserve this tool for open-ended questions that span the codebase, or tasks that match an available agent type.
668
+
669
+ ## Usage notes
670
+
671
+ - Always include a short (3-5 word) description summarizing what the agent will do (shown in UI).
672
+ - When you launch multiple agents for independent work, send them in a single message with multiple tool uses, with run_in_background: true on each, so they run concurrently. If the user specifies that they want agents run "in parallel", you MUST send a single message with multiple tool calls. Foreground calls run sequentially — only one executes at a time.
673
+ - When the agent is done, it returns a single message back to you. The result is not visible to the user — to show the user, send a text message with a concise summary.
674
+ - Trust but verify: an agent's summary describes what it intended to do, not necessarily what it did. When an agent writes or edits code, check the actual changes before reporting work as done.
675
+ - Use run_in_background for work you don't need immediately. You will be notified when it completes — do NOT poll or sleep waiting for it. Continue with other work or respond to the user instead.
676
+ - Foreground vs background: use foreground (default) when you need the agent's results before you can proceed. Use background when you have genuinely independent work to do in parallel.
677
+ - Use resume with an agent ID to continue a previous agent's work. A new (non-resume) Agent call starts a fresh agent with no memory of prior runs, so the prompt must be self-contained.
662
678
  - Use steer_subagent to send mid-run messages to a running background agent.
679
+ - Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, etc.), since it is not aware of the user's intent.
680
+ - If an agent's description says it should be used proactively, try to use it without the user having to ask for it first.
663
681
  - Use model to specify a different model (as "provider/modelId", or fuzzy e.g. "haiku", "sonnet").
664
682
  - Use thinking to control extended thinking level.
665
683
  - Use inherit_context if the agent needs the parent conversation history.
666
- - Use isolation: "worktree" to run the agent in an isolated git worktree (safe parallel file modifications).${scheduleGuideline}`,
684
+ - Use isolation: "worktree" to run the agent in an isolated git worktree (safe parallel file modifications). The worktree is automatically cleaned up if the agent makes no changes; otherwise the path and branch are returned in the result.${scheduleGuideline}
685
+
686
+ ## Writing the prompt
687
+
688
+ Provide clear, detailed prompts so the agent can work autonomously. Brief it like a smart colleague who just walked into the room — it hasn't seen this conversation, doesn't know what you've tried, doesn't understand why this task matters.
689
+ - Explain what you're trying to accomplish and why.
690
+ - Describe what you've already learned or ruled out.
691
+ - Give enough context about the surrounding problem that the agent can make judgment calls rather than just following a narrow instruction.
692
+ - If you need a short response, say so ("report in under 200 words").
693
+ - Lookups: hand over the exact command. Investigations: hand over the question — prescribed steps become dead weight when the premise is wrong.
694
+
695
+ Terse command-style prompts produce shallow, generic work.
696
+
697
+ **Never delegate understanding.** Don't write "based on your findings, fix the bug" or "based on the research, implement it." Those phrases push synthesis onto the agent instead of doing it yourself. Write prompts that prove you understood: include file paths, line numbers, what specifically to change.`,
667
698
  parameters: Type.Object({
668
699
  prompt: Type.String({
669
700
  description: "The task for the agent to perform.",
@@ -842,6 +873,35 @@ Guidelines:
842
873
  }
843
874
  }
844
875
 
876
+ // Scope validation: the effective resolved model is checked against the
877
+ // user's enabledModels list (read in `enabled-models.ts`).
878
+ //
879
+ // Design: scopeModels guards against *runtime* LLM choices, not user-level config.
880
+ // - Caller-supplied out-of-scope → hard error (the orchestrator made an explicit
881
+ // out-of-scope choice; surface it so it picks differently).
882
+ // - Frontmatter-pinned or parent-inherited out-of-scope → warn but proceed (the
883
+ // user authored/installed this agent or chose the parent's model; trust it).
884
+ // See SubagentsSettings.scopeModels docstring for the full policy.
885
+ if (isScopeModelsEnabled() && model) {
886
+ const allowed = resolveEnabledModels(readEnabledModels(ctx.cwd), ctx.modelRegistry, ctx.cwd);
887
+ if (allowed && !isModelInScope(model, allowed)) {
888
+ if (resolvedConfig.modelFromParams) {
889
+ const list = [...allowed].sort().map(m => ` ${m}`).join("\n");
890
+ return textResult(
891
+ `Model not in scope: "${resolvedConfig.modelInput}".\n\n` +
892
+ `Allowed models (from enabledModels):\n${list}`,
893
+ );
894
+ }
895
+ // Frontmatter-pinned or parent-inherited: warn + proceed.
896
+ const agentLabel = customConfig?.displayName ?? subagentType;
897
+ const modelLabel = resolvedConfig.modelInput ?? `${model.provider}/${model.id}`;
898
+ ctx.ui.notify(
899
+ `Agent "${agentLabel}" using out-of-scope model "${modelLabel}"`,
900
+ "warning",
901
+ );
902
+ }
903
+ }
904
+
845
905
  const thinking = resolvedConfig.thinking;
846
906
  const inheritContext = resolvedConfig.inheritContext;
847
907
  const runInBackground = resolvedConfig.runInBackground;
@@ -1515,7 +1575,7 @@ Guidelines:
1515
1575
 
1516
1576
  // Build the .md file content
1517
1577
  const fmFields: string[] = [];
1518
- fmFields.push(`description: ${cfg.description}`);
1578
+ fmFields.push(`description: ${JSON.stringify(cfg.description)}`);
1519
1579
  if (cfg.displayName) fmFields.push(`display_name: ${cfg.displayName}`);
1520
1580
  fmFields.push(`tools: ${cfg.builtinToolNames?.join(", ") || "all"}`);
1521
1581
  if (cfg.model) fmFields.push(`model: ${cfg.model}`);
@@ -1790,76 +1850,91 @@ ${systemPrompt}
1790
1850
  graceTurns: getGraceTurns(),
1791
1851
  defaultJoinMode: getDefaultJoinMode(),
1792
1852
  schedulingEnabled: isSchedulingEnabled(),
1853
+ scopeModels: isScopeModelsEnabled(),
1793
1854
  };
1794
1855
  }
1795
1856
 
1857
+ const NUMERIC_IDS = new Set(["maxConcurrent", "defaultMaxTurns", "graceTurns"]);
1858
+
1796
1859
  async function showSettings(ctx: ExtensionCommandContext) {
1797
- const choice = await ctx.ui.select("Settings", [
1798
- `Max concurrency (current: ${manager.getMaxConcurrent()})`,
1799
- `Default max turns (current: ${getDefaultMaxTurns() ?? "unlimited"})`,
1800
- `Grace turns (current: ${getGraceTurns()})`,
1801
- `Join mode (current: ${getDefaultJoinMode()})`,
1802
- `Scheduling (current: ${isSchedulingEnabled() ? "enabled" : "disabled"})`,
1803
- ]);
1804
- if (!choice) return;
1860
+ function buildItems(): SettingItem[] {
1861
+ const mc = manager.getMaxConcurrent();
1862
+ const dmt = getDefaultMaxTurns() ?? 0;
1863
+ const gt = getGraceTurns();
1864
+
1865
+ return [
1866
+ {
1867
+ id: "maxConcurrent",
1868
+ label: "Max concurrency",
1869
+ description: "Max concurrent background agents (Enter to type)",
1870
+ currentValue: String(mc),
1871
+ values: [String(mc)],
1872
+ },
1873
+ {
1874
+ id: "defaultMaxTurns",
1875
+ label: "Default max turns",
1876
+ description: "Default max turns before wrap-up (0 = unlimited, Enter to type)",
1877
+ currentValue: String(dmt),
1878
+ values: [String(dmt)],
1879
+ },
1880
+ {
1881
+ id: "graceTurns",
1882
+ label: "Grace turns",
1883
+ description: "Grace turns after wrap-up steer (Enter to type)",
1884
+ currentValue: String(gt),
1885
+ values: [String(gt)],
1886
+ },
1887
+ {
1888
+ id: "joinMode",
1889
+ label: "Join mode",
1890
+ description: "Default join mode for background agents",
1891
+ currentValue: getDefaultJoinMode(),
1892
+ values: ["smart", "async", "group"],
1893
+ },
1894
+ {
1895
+ id: "schedulingEnabled",
1896
+ label: "Scheduling",
1897
+ description: "Schedule subagent feature (off removes `schedule` param from Agent tool spec on next pi session)",
1898
+ currentValue: isSchedulingEnabled() ? "on" : "off",
1899
+ values: ["on", "off"],
1900
+ },
1901
+ {
1902
+ id: "scopeModels",
1903
+ label: "Scope models",
1904
+ description: "Validate subagent models against scoped models (/scoped-models)",
1905
+ currentValue: isScopeModelsEnabled() ? "on" : "off",
1906
+ values: ["on", "off"],
1907
+ },
1908
+ ];
1909
+ }
1805
1910
 
1806
- if (choice.startsWith("Max concurrency")) {
1807
- const val = await ctx.ui.input("Max concurrent background agents", String(manager.getMaxConcurrent()));
1808
- if (val) {
1809
- const n = parseInt(val, 10);
1911
+ function applyValue(id: string, value: string) {
1912
+ if (id === "maxConcurrent") {
1913
+ const n = parseInt(value, 10);
1810
1914
  if (n >= 1) {
1811
1915
  manager.setMaxConcurrent(n);
1812
1916
  notifyApplied(ctx, `Max concurrency set to ${n}`);
1813
- } else {
1814
- ctx.ui.notify("Must be a positive integer.", "warning");
1815
1917
  }
1816
- }
1817
- } else if (choice.startsWith("Default max turns")) {
1818
- const val = await ctx.ui.input("Default max turns before wrap-up (0 = unlimited)", String(getDefaultMaxTurns() ?? 0));
1819
- if (val) {
1820
- const n = parseInt(val, 10);
1918
+ } else if (id === "defaultMaxTurns") {
1919
+ const n = parseInt(value, 10);
1821
1920
  if (n === 0) {
1822
1921
  setDefaultMaxTurns(undefined);
1823
1922
  notifyApplied(ctx, "Default max turns set to unlimited");
1824
1923
  } else if (n >= 1) {
1825
1924
  setDefaultMaxTurns(n);
1826
1925
  notifyApplied(ctx, `Default max turns set to ${n}`);
1827
- } else {
1828
- ctx.ui.notify("Must be 0 (unlimited) or a positive integer.", "warning");
1829
1926
  }
1830
- }
1831
- } else if (choice.startsWith("Grace turns")) {
1832
- const val = await ctx.ui.input("Grace turns after wrap-up steer", String(getGraceTurns()));
1833
- if (val) {
1834
- const n = parseInt(val, 10);
1927
+ } else if (id === "graceTurns") {
1928
+ const n = parseInt(value, 10);
1835
1929
  if (n >= 1) {
1836
1930
  setGraceTurns(n);
1837
1931
  notifyApplied(ctx, `Grace turns set to ${n}`);
1838
- } else {
1839
- ctx.ui.notify("Must be a positive integer.", "warning");
1840
1932
  }
1841
- }
1842
- } else if (choice.startsWith("Join mode")) {
1843
- const val = await ctx.ui.select("Default join mode for background agents", [
1844
- "smart auto-group 2+ agents in same turn (default)",
1845
- "async always notify individually",
1846
- "group — always group background agents",
1847
- ]);
1848
- if (val) {
1849
- const mode = val.split(" ")[0] as JoinMode;
1850
- setDefaultJoinMode(mode);
1851
- notifyApplied(ctx, `Default join mode set to ${mode}`);
1852
- }
1853
- } else if (choice.startsWith("Scheduling")) {
1854
- const val = await ctx.ui.select(
1855
- "Schedule subagent feature",
1856
- [
1857
- "enabled — Agent tool accepts a `schedule` param; /agents → Scheduled jobs visible",
1858
- "disabled — `schedule` removed from Agent tool spec (no LLM-context cost); menu hidden",
1859
- ],
1860
- );
1861
- if (val) {
1862
- const enabled = val.startsWith("enabled");
1933
+ } else if (id === "joinMode") {
1934
+ setDefaultJoinMode(value as JoinMode);
1935
+ notifyApplied(ctx, `Default join mode set to ${value}`);
1936
+ } else if (id === "schedulingEnabled") {
1937
+ const enabled = value === "on";
1863
1938
  if (enabled === isSchedulingEnabled()) {
1864
1939
  ctx.ui.notify(`Scheduling already ${enabled ? "enabled" : "disabled"}.`, "info");
1865
1940
  } else {
@@ -1870,6 +1945,84 @@ ${systemPrompt}
1870
1945
  `Scheduling ${enabled ? "enabled" : "disabled"}. Tool spec change takes effect on next pi session.`,
1871
1946
  );
1872
1947
  }
1948
+ } else if (id === "scopeModels") {
1949
+ const enabled = value === "on";
1950
+ setScopeModelsEnabled(enabled);
1951
+ notifyApplied(ctx, `Scope models ${enabled ? "enabled" : "disabled"}`);
1952
+ }
1953
+ }
1954
+
1955
+ let list: SettingsList;
1956
+ // Track current selection index directly (SettingsList doesn't expose it).
1957
+ // Updated on arrow keys so Enter knows which field is selected immediately.
1958
+ let currentIndex = 0;
1959
+
1960
+ const result = await ctx.ui.custom<string | undefined>((_tui, _theme, _kb, done) => {
1961
+ const items = buildItems();
1962
+
1963
+ list = new SettingsList(
1964
+ items,
1965
+ items.length + 2,
1966
+ getSettingsListTheme(),
1967
+ (id, newValue) => {
1968
+ applyValue(id, newValue);
1969
+ },
1970
+ () => done(undefined as undefined),
1971
+ );
1972
+
1973
+ const container = new Container();
1974
+ container.addChild(new Text("⚙ Subagent Settings", 0, 0));
1975
+ container.addChild(new Spacer(1));
1976
+ container.addChild(list);
1977
+
1978
+ return {
1979
+ render: (w: number) => container.render(w),
1980
+ invalidate: () => container.invalidate(),
1981
+ handleInput: (data: string) => {
1982
+ // Track navigation so Enter knows the current field
1983
+ if (matchesKey(data, "up")) {
1984
+ currentIndex = Math.max(0, currentIndex - 1);
1985
+ } else if (matchesKey(data, "down")) {
1986
+ currentIndex = Math.min(items.length - 1, currentIndex + 1);
1987
+ }
1988
+
1989
+ // Enter on numeric field → close and prompt for typed input
1990
+ if (matchesKey(data, Key.enter) && NUMERIC_IDS.has(items[currentIndex].id)) {
1991
+ done(items[currentIndex].id);
1992
+ return;
1993
+ }
1994
+ list.handleInput?.(data);
1995
+ },
1996
+ };
1997
+ });
1998
+
1999
+ // If a numeric field ID was returned, prompt for typed input
2000
+ if (result && NUMERIC_IDS.has(result)) {
2001
+ const current = result === "maxConcurrent"
2002
+ ? String(manager.getMaxConcurrent())
2003
+ : result === "defaultMaxTurns"
2004
+ ? String(getDefaultMaxTurns() ?? 0)
2005
+ : String(getGraceTurns());
2006
+
2007
+ const label = result === "maxConcurrent"
2008
+ ? "Max concurrency (1+)"
2009
+ : result === "defaultMaxTurns"
2010
+ ? "Default max turns (0 = unlimited)"
2011
+ : "Grace turns (1+)";
2012
+
2013
+ // Loop until user enters a valid integer or cancels (Esc / null).
2014
+ // Silently trims whitespace; rejects non-numeric input by re-prompting.
2015
+ let input: string | undefined = await ctx.ui.input(label, current);
2016
+ while (input != null) {
2017
+ const trimmed = input.trim();
2018
+ const n = Number(trimmed);
2019
+ if (trimmed !== "" && Number.isInteger(n)) {
2020
+ applyValue(result, String(n));
2021
+ await showSettings(ctx);
2022
+ return;
2023
+ }
2024
+ // Invalid — re-prompt with the user's last entry so they can edit it
2025
+ input = await ctx.ui.input(label, trimmed);
1873
2026
  }
1874
2027
  }
1875
2028
  }
@@ -8,7 +8,7 @@
8
8
  import { appendFileSync, chmodSync, mkdirSync, writeFileSync } from "node:fs";
9
9
  import { tmpdir } from "node:os";
10
10
  import { join } from "node:path";
11
- import type { AgentSession, AgentSessionEvent } from "@mariozechner/pi-coding-agent";
11
+ import type { AgentSession, AgentSessionEvent } from "@earendil-works/pi-coding-agent";
12
12
 
13
13
  /**
14
14
  * Encode a cwd path as a filesystem-safe directory name. Handles:
@@ -62,10 +62,14 @@ export class ScheduleStore {
62
62
  constructor(filePath: string) {
63
63
  this.filePath = filePath;
64
64
  this.lockPath = filePath + ".lock";
65
- mkdirSync(dirname(filePath), { recursive: true });
66
65
  this.load();
67
66
  }
68
67
 
68
+ /** Create the backing directory lazily — only when we're about to persist. */
69
+ private ensureDir(): void {
70
+ mkdirSync(dirname(this.filePath), { recursive: true });
71
+ }
72
+
69
73
  /** Load from disk into the in-memory cache. Silent on parse errors. */
70
74
  private load(): void {
71
75
  if (!existsSync(this.filePath)) return;
@@ -86,6 +90,7 @@ export class ScheduleStore {
86
90
 
87
91
  /** Acquire lock → reload → mutate → save → release. */
88
92
  private withLock<T>(fn: () => T): T {
93
+ this.ensureDir();
89
94
  acquireLock(this.lockPath);
90
95
  try {
91
96
  this.load();
@@ -121,6 +126,9 @@ export class ScheduleStore {
121
126
  }
122
127
 
123
128
  update(id: string, patch: Partial<ScheduledSubagent>): ScheduledSubagent | undefined {
129
+ // No-op fast path — an unknown id changes nothing, so don't lock or touch
130
+ // disk (which would otherwise lazily create the backing directory).
131
+ if (!this.jobs.has(id)) return undefined;
124
132
  return this.withLock(() => {
125
133
  const existing = this.jobs.get(id);
126
134
  if (!existing) return undefined;
@@ -131,6 +139,8 @@ export class ScheduleStore {
131
139
  }
132
140
 
133
141
  remove(id: string): boolean {
142
+ // No-op fast path — see update().
143
+ if (!this.jobs.has(id)) return false;
134
144
  return this.withLock(() => this.jobs.delete(id));
135
145
  }
136
146
 
package/src/schedule.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  * `subagent-notification` followUp path. No new delivery code.
16
16
  */
17
17
 
18
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
18
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
19
19
  import { Cron } from "croner";
20
20
  import { nanoid } from "nanoid";
21
21
  import type { AgentManager } from "./agent-manager.js";
package/src/settings.ts CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
6
  import { dirname, join } from "node:path";
7
- import { getAgentDir } from "@mariozechner/pi-coding-agent";
7
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
8
8
  import type { JoinMode } from "./types.js";
9
9
 
10
10
  export interface SubagentsSettings {
@@ -26,6 +26,28 @@ export interface SubagentsSettings {
26
26
  * (next pi session); runtime menu/runtime-fire short-circuit is immediate.
27
27
  */
28
28
  schedulingEnabled?: boolean;
29
+ /**
30
+ * When true, the effective model of each subagent spawn is validated
31
+ * against `enabledModels` from pi's settings — both global
32
+ * (`<agentDir>/settings.json`) and project-local (`<cwd>/.pi/settings.json`),
33
+ * with project overriding global (mirrors pi's SettingsManager deep-merge).
34
+ *
35
+ * scopeModels guards against runtime LLM choices, not user-level config.
36
+ * Out-of-scope handling reflects this:
37
+ * - Caller-supplied via `Agent({ model: "..." })` (only when frontmatter
38
+ * has no `model:`, since frontmatter is authoritative): hard error
39
+ * returned to the orchestrator, listing the allowed models. The LLM
40
+ * made an explicit out-of-scope choice and gets explicit feedback.
41
+ * - Frontmatter-pinned: warning toast + the pinned model runs. The
42
+ * agent's author/installer chose this; trust it.
43
+ * - Parent-inherited (neither caller nor frontmatter sets a model):
44
+ * warning toast + parent's model runs. The user chose the parent's
45
+ * model when starting the session; trust it.
46
+ *
47
+ * No-op when pi's `enabledModels` is empty or absent — nothing to validate
48
+ * against. Defaults to false: subagents may use any model.
49
+ */
50
+ scopeModels?: boolean;
29
51
  }
30
52
 
31
53
  /** Setter hooks used by applySettings to wire persisted values into in-memory state. */
@@ -35,6 +57,7 @@ export interface SettingsAppliers {
35
57
  setGraceTurns: (n: number) => void;
36
58
  setDefaultJoinMode: (mode: JoinMode) => void;
37
59
  setSchedulingEnabled: (b: boolean) => void;
60
+ setScopeModels: (enabled: boolean) => void;
38
61
  }
39
62
 
40
63
  /** Emit callback — a subset of `pi.events.emit` to keep helpers testable. */
@@ -81,6 +104,9 @@ function sanitize(raw: unknown): SubagentsSettings {
81
104
  if (typeof r.schedulingEnabled === "boolean") {
82
105
  out.schedulingEnabled = r.schedulingEnabled;
83
106
  }
107
+ if (typeof r.scopeModels === "boolean") {
108
+ out.scopeModels = r.scopeModels;
109
+ }
84
110
  return out;
85
111
  }
86
112
 
@@ -136,6 +162,7 @@ export function applySettings(s: SubagentsSettings, appliers: SettingsAppliers):
136
162
  if (typeof s.graceTurns === "number") appliers.setGraceTurns(s.graceTurns);
137
163
  if (s.defaultJoinMode) appliers.setDefaultJoinMode(s.defaultJoinMode);
138
164
  if (typeof s.schedulingEnabled === "boolean") appliers.setSchedulingEnabled(s.schedulingEnabled);
165
+ if (typeof s.scopeModels === "boolean") appliers.setScopeModels(s.scopeModels);
139
166
  }
140
167
 
141
168
  /**
@@ -22,7 +22,7 @@ import type { Dirent } from "node:fs";
22
22
  import { existsSync, readdirSync } from "node:fs";
23
23
  import { homedir } from "node:os";
24
24
  import { join } from "node:path";
25
- import { getAgentDir } from "@mariozechner/pi-coding-agent";
25
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
26
26
  import { isSymlink, isUnsafeName, safeReadFile } from "./memory.js";
27
27
 
28
28
  export interface PreloadedSkill {
package/src/types.ts CHANGED
@@ -2,8 +2,8 @@
2
2
  * types.ts — Type definitions for the subagent system.
3
3
  */
4
4
 
5
- import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
6
- import type { AgentSession } from "@mariozechner/pi-coding-agent";
5
+ import type { ThinkingLevel } from "@earendil-works/pi-ai";
6
+ import type { AgentSession } from "@earendil-works/pi-coding-agent";
7
7
  import type { LifetimeUsage } from "./usage.js";
8
8
 
9
9
  export type { ThinkingLevel };
@@ -5,7 +5,7 @@
5
5
  * Uses the callback form of setWidget for themed rendering.
6
6
  */
7
7
 
8
- import { truncateToWidth } from "@mariozechner/pi-tui";
8
+ import { truncateToWidth } from "@earendil-works/pi-tui";
9
9
  import type { AgentManager } from "../agent-manager.js";
10
10
  import { getConfig } from "../agent-types.js";
11
11
  import type { AgentInvocation, SubagentType } from "../types.js";
@@ -5,8 +5,8 @@
5
5
  * Subscribes to session events for real-time streaming updates.
6
6
  */
7
7
 
8
- import type { AgentSession } from "@mariozechner/pi-coding-agent";
9
- import { type Component, matchesKey, type TUI, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
8
+ import type { AgentSession } from "@earendil-works/pi-coding-agent";
9
+ import { type Component, matchesKey, type TUI, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
10
10
  import { extractText } from "../context.js";
11
11
  import type { AgentRecord } from "../types.js";
12
12
  import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
@@ -8,7 +8,7 @@
8
8
  * if real demand emerges.
9
9
  */
10
10
 
11
- import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
11
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
12
12
  import type { SubagentScheduler } from "../schedule.js";
13
13
  import type { ScheduledSubagent } from "../types.js";
14
14