pi-subagents-j0k3r 1.0.1 → 1.1.1

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
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.0 - 2026-06-27
4
+
5
+ ### Added
6
+ - Added support for loading markdown-defined subagents from both `agents` and `subagents` directories globally and project-locally.
7
+ - Added startup warnings when duplicate names exist in `agents` and `subagents` at the same scope, while preserving `subagents` as the winning source.
8
+ - Enabled project Skill Registry configuration and ignored generated registry cache outputs.
9
+
10
+ ### Fixed
11
+ - Made `model_profiles` global-only so project-local `.pi/subagents.json` cannot override per-agent model/effort routing.
12
+
13
+ ### Documentation
14
+ - Updated README and Subagents configuration skill guidance for `agents`/`subagents` source precedence and global-only model profiles.
15
+
3
16
  ## 1.0.1 - 2026-06-27
4
17
 
5
18
  ### Fixed
package/README.md CHANGED
@@ -67,10 +67,14 @@ Subagents are markdown files with optional YAML-like frontmatter.
67
67
 
68
68
  Load order:
69
69
 
70
- 1. Global user subagents from `$PI_CODING_AGENT_DIR/subagents/*.md`.
71
- 2. Project subagents from `.pi/subagents/*.md`.
70
+ 1. Global user agents from `$PI_CODING_AGENT_DIR/agents/*.md`.
71
+ 2. Global user subagents from `$PI_CODING_AGENT_DIR/subagents/*.md`.
72
+ 3. Project agents from `.pi/agents/*.md`.
73
+ 4. Project subagents from `.pi/subagents/*.md`.
72
74
 
73
- Project definitions override global definitions with the same normalized name.
75
+ Project definitions override global definitions with the same normalized name. Within the same scope, `subagents` definitions override `agents` definitions with the same normalized name and Pi shows a startup warning so the duplicate can be cleaned up.
76
+
77
+ The npm package is the extension runtime only. It does not ship or load subagent definitions from `node_modules/pi-subagents-j0k3r/agents`; use the directories above, or run `subagent_list_agents` / `subagent({ action: "list" })` to inspect the definitions Pi actually loaded.
74
78
 
75
79
  Default global agent directory:
76
80
 
@@ -127,7 +131,7 @@ Config files:
127
131
  .pi/subagents.json # project
128
132
  ```
129
133
 
130
- Config resolves as a cascade: project `.pi/subagents.json` overrides global `~/.pi/agent/subagents.json`; missing project fields fall back to global config; fields missing from both fall back to built-in defaults. Model profiles are merged by normalized lowercase agent name, with project profile fields overriding global profile fields.
134
+ Config resolves as a cascade: project `.pi/subagents.json` overrides global `~/.pi/agent/subagents.json`; missing project fields fall back to global config; fields missing from both fall back to built-in defaults. `model_profiles` are scoped to the definition source: project-local definitions use `.pi/subagents.json`, while global definitions use the global `subagents.json`. If a project definition overrides a global definition with the same name, the project definition and its project-local profile win.
131
135
 
132
136
  Example:
133
137
 
@@ -168,7 +172,7 @@ Example:
168
172
  |---|---:|---|
169
173
  | `default_model` | current orchestrator model | Fallback model for all subagents. Format: `provider/model-id`. |
170
174
  | `default_effort` | current orchestrator effort | Fallback thinking effort. Also accepts `default_thinking_level` or `thinkingLevel`. |
171
- | `model_profiles` | `{}` | Per-agent model/effort overrides. |
175
+ | `model_profiles` | `{}` | Per-agent model/effort overrides scoped to matching definitions. Project-local profiles apply to project-local definitions; global profiles apply to global definitions. |
172
176
  | `timeout_ms` | `600000` | Total timeout per subagent task. |
173
177
  | `stall_timeout_ms` | `120000` | Inactivity timeout for a subagent session. |
174
178
  | `max_concurrency` | `5` | Max concurrent subagent tasks per cwd/config pair. |
@@ -200,7 +204,7 @@ any tool starting with subagent_
200
204
 
201
205
  Effective model resolution order:
202
206
 
203
- 1. `model_profiles[agent].model`
207
+ 1. `model_profiles[agent].model` from the config matching the selected definition scope: project-local for project definitions, global for global definitions
204
208
  2. subagent frontmatter `model`
205
209
  3. `default_model`
206
210
  4. current orchestrator model
@@ -208,7 +212,7 @@ Effective model resolution order:
208
212
 
209
213
  Effective effort resolution order:
210
214
 
211
- 1. `model_profiles[agent].effort`
215
+ 1. `model_profiles[agent].effort` from the config matching the selected definition scope: project-local for project definitions, global for global definitions
212
216
  2. subagent frontmatter `effort` / `thinking_level` / `thinkingLevel`
213
217
  3. `default_effort`
214
218
  4. current orchestrator thinking level
@@ -282,20 +286,14 @@ Behavior:
282
286
  | Entry point | Description |
283
287
  |---|---|
284
288
  | `/subagents` | Open the session-focused TUI subagent history panel. |
285
- | `/subagent-models` | Configure global subagent and SDD phase model profiles. |
289
+ | `/subagent-models` | Configure subagent and SDD phase model profiles in the matching local or global config. |
286
290
  | `ctrl+,` | Open the TUI subagent history panel in OpenCode mode by default. Configurable via `history_panel_shortcut` in `subagents.json`. |
287
291
  | `x` | Cancel the currently selected queued/running subagent from the open history/detail panel by default. Configurable via `detail_cancel_shortcut` in `subagents.json`. |
288
292
  | `ctrl+h` | Send the running Claude-mode subagent task to the background by default. Configurable via `background_handoff_shortcut` in `subagents.json`. |
289
293
 
290
- `/subagent-models` writes global profile changes to:
291
-
292
- ```txt
293
- ~/.pi/agent/subagents.json
294
- ```
295
-
296
- or `$PI_CODING_AGENT_DIR/subagents.json` when `PI_CODING_AGENT_DIR` is set.
294
+ `/subagent-models` writes profile changes to the config that matches each selected definition: project-local subagents write to `.pi/subagents.json`, while global subagents and synthetic SDD phase rows write to `~/.pi/agent/subagents.json` or `$PI_CODING_AGENT_DIR/subagents.json` when `PI_CODING_AGENT_DIR` is set.
297
295
 
298
- In non-TUI environments, edit `model_profiles` manually in that JSON file.
296
+ In non-TUI environments, edit `model_profiles` manually in the matching local or global JSON file.
299
297
 
300
298
  ## Task history
301
299
 
@@ -392,7 +390,7 @@ This package bundles:
392
390
  - `index.ts` and `src/**` — the Pi extension runtime.
393
391
  - `skills/subagents-configuration/SKILL.md` — configuration guidance for agents that need to create or edit subagent definitions.
394
392
 
395
- Subagent definitions are intentionally user/project configuration, not hard-coded package behavior. Add them globally in `$PI_CODING_AGENT_DIR/subagents/*.md` or project-locally in `.pi/subagents/*.md`.
393
+ Subagent definitions are intentionally user/project configuration, not hard-coded package behavior. Add them globally in `$PI_CODING_AGENT_DIR/agents/*.md` or `$PI_CODING_AGENT_DIR/subagents/*.md`, or project-locally in `.pi/agents/*.md` or `.pi/subagents/*.md`. Do not inspect `node_modules/pi-subagents-j0k3r/agents` for definitions; that path is not part of the package design and may not exist.
396
394
 
397
395
  ## Development
398
396
 
package/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { SubagentManager } from './src/manager.js';
2
- import { readSubagentsConfig } from './src/config.js';
2
+ import { readSubagentsConfig, subagentSourceWarnings } from './src/config.js';
3
3
  import { registerSubagentTools, triggerClaudeBackgroundHandoff } from './src/tools.js';
4
4
  import { runSubagentModelsCommand } from './src/model-profiles-ui.js';
5
5
  import { SubagentsHistoryPanel } from './src/ui.js';
@@ -412,6 +412,8 @@ export default function subagentsExtension(pi: any): void {
412
412
 
413
413
  pi.on?.('session_start', (_event: unknown, ctx: any) => {
414
414
  clearClaudeBackgroundWidget();
415
+ const cwd = ctx?.cwd ?? process.cwd();
416
+ for (const warning of subagentSourceWarnings(cwd)) ctx?.ui?.notify?.(warning, 'warning');
415
417
  if (typeof ctx?.ui?.setWidget !== 'function') return;
416
418
  widgetCtx = ctx;
417
419
  if (!installClaudeBackgroundWidget(ctx)) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents-j0k3r",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "description": "Installable Pi package that adds markdown-defined subagents, delegated task tools, history, and model profiles.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -19,10 +19,13 @@ Use this block as the machine-readable source for `.pi/skill-registry.json` gene
19
19
  "domains": ["subagents-configuration", "subagent-config", "model-profile-config", "tool-allowlist-config", "subagent-history-config", "subagent-shortcut-config"],
20
20
  "triggers": {
21
21
  "paths": [
22
+ ".pi/agents/**/*.md",
22
23
  ".pi/subagents/**/*.md",
23
24
  ".pi/subagents.json",
25
+ "agents/**/*.md",
24
26
  "subagents/**/*.md",
25
27
  "subagents.json",
28
+ "~/.pi/agent/agents/**/*.md",
26
29
  "~/.pi/agent/subagents/**/*.md",
27
30
  "~/.pi/agent/subagents.json"
28
31
  ],
@@ -88,11 +91,12 @@ Do not load this skill for ordinary subagent delegation/use (`subagent_run`, tas
88
91
  - Prefer narrow tool allowlists per subagent. Do not grant write/bash tools unless the subagent purpose requires them.
89
92
  - For SDD/PRD phase agents, prefer deterministic active-flow memory tools only: `memory_search`, `memory_get`, `memory_add`, and `memory_update`; avoid `memory_context` and `memory_recall` in subagent allowlists unless there is a specific reviewed need.
90
93
  - For SDD phase agents, memory write tools may be allowed only for active SDD flow memory/artifacts according to `sdd-workflow`.
91
- - Project subagents live in `.pi/subagents/*.md`; global user subagents live in `$PI_CODING_AGENT_DIR/subagents/*.md` or `~/.pi/agent/subagents/*.md`.
92
- - Project definitions override global definitions with the same normalized name.
94
+ - Project subagent definitions live in `.pi/agents/*.md` and `.pi/subagents/*.md`; global user definitions live in `$PI_CODING_AGENT_DIR/agents/*.md`, `$PI_CODING_AGENT_DIR/subagents/*.md`, `~/.pi/agent/agents/*.md`, or `~/.pi/agent/subagents/*.md`.
95
+ - The npm package is the extension runtime only; do not tell users or future agents to inspect `node_modules/pi-subagents-j0k3r/agents` for subagent definitions. Use the real global/project definition directories above, or runtime listing via `subagent_list_agents` / `subagent({ action: "list" })`.
96
+ - Project definitions override global definitions with the same normalized name. Within the same scope, definitions in `subagents` override definitions in `agents` with the same normalized name, and Pi should warn at session startup so users can clean up the duplicate.
93
97
  - Subagents config resolves as a cascade: project `.pi/subagents.json` overrides global `$PI_CODING_AGENT_DIR/subagents.json` or `~/.pi/agent/subagents.json`; missing project fields fall back to global config; fields missing from both fall back to built-in defaults. Communicate this precedence to users when explaining config behavior.
94
- - `model_profiles` are deep-merged by agent name with project-local profile fields taking precedence over global profile fields.
95
- - Prefer configuring subagent `model` and `effort` in global or project `subagents.json` under `model_profiles`, not in the subagent markdown frontmatter. Markdown definitions should usually contain identity, description, tool allowlist, and behavioral instructions only.
98
+ - `model_profiles` are scoped to the matching subagent definition source: project-local profile entries in `.pi/subagents.json` apply to project-local definitions, while global profile entries apply to global definitions. If a project definition overrides a global definition with the same normalized name, the project definition and its project-local profile win.
99
+ - Prefer configuring subagent `model` and `effort` under `model_profiles` in the config matching the definition scope: project-local definitions use `.pi/subagents.json`; global definitions use `$PI_CODING_AGENT_DIR/subagents.json` or `~/.pi/agent/subagents.json`. Markdown definitions should usually contain identity, description, tool allowlist, and behavioral instructions only.
96
100
  - Nested subagent sessions should use `session_resources: "lean"` by default so the subagent markdown body becomes the nested session system prompt, the delegated user prompt contains only orchestrator context/task, and workflow skills, prompt templates, themes, context files, and startup context injections are not auto-loaded.
97
101
  - In lean mode, extensions are loaded for allowlisted tools and tool-safety hooks only; prompt/context lifecycle hooks such as `before_agent_start` and `context` must not inject hidden messages into subagent turns.
98
102
  - Subagent task history is stored globally under data storage, but rows remain project-scoped by `cwd`; history stores delegated prompt and subagent system prompt separately.
@@ -151,7 +155,7 @@ tools:
151
155
  Instructions...
152
156
  ```
153
157
 
154
- Configure routing separately in `subagents.json` when needed. If no profile/default is configured, the subagent inherits the current orchestrator model and thinking effort.
158
+ Configure model/effort routing separately in the matching local or global `subagents.json` when needed. If no matching profile/default is configured, the subagent inherits the current orchestrator model and thinking effort.
155
159
 
156
160
  ```json
157
161
  {
@@ -166,9 +170,9 @@ Configure routing separately in `subagents.json` when needed. If no profile/defa
166
170
 
167
171
  Model/effort resolution order:
168
172
 
169
- 1. `model_profiles[agentName]` in `subagents.json`.
173
+ 1. `model_profiles[agentName]` from the config matching the selected definition scope: `.pi/subagents.json` for project-local definitions, or `$PI_CODING_AGENT_DIR/subagents.json` / `~/.pi/agent/subagents.json` for global definitions.
170
174
  2. Markdown frontmatter `model` / `effort` only for explicit per-file overrides.
171
- 3. `default_model` / `default_effort` in `subagents.json`.
175
+ 3. `default_model` / `default_effort` from effective `subagents.json` config.
172
176
  4. Current orchestrator model / effort.
173
177
 
174
178
  ## Decision Gates
@@ -176,16 +180,16 @@ Model/effort resolution order:
176
180
  - If the subagent will modify files, run bash, or write memory, ask whether a full SDD workflow or stricter review is required.
177
181
  - If the subagent needs human input, require a structured `interaction_required` request with enough prompt, payload, and expected-response data for the parent to answer.
178
182
  - If a project wants many subagents or broad tools, recommend starting with read-only discovery agents and expanding deliberately.
179
- - If model profiles are global, confirm the user wants global behavior rather than project-only config.
183
+ - If the user asks for model profiles, choose the config that matches the subagent definition scope: project-local profiles for project-local definitions and global profiles for global definitions or synthetic SDD phase rows.
180
184
 
181
185
  ## Execution Steps
182
186
 
183
187
  1. Identify target scope: npm package install/update, global subagent, project subagent, global config, or project config, and explain the cascade when relevant: project-local config first, then global config for missing fields, then built-in defaults.
184
188
  2. For package setup, inspect settings before editing; use `pi install npm:pi-subagents-j0k3r` when possible, or edit `~/.pi/agent/settings.json` only when the CLI is unavailable/broken. Prefer unpinned `npm:pi-subagents-j0k3r` unless the user asks for a fixed version.
185
- 3. Read existing subagent markdown/config before editing.
186
- 4. For new subagents, choose lowercase kebab-case names and clear trigger-focused descriptions.
189
+ 3. Read existing subagent markdown/config before editing, checking both `agents` and `subagents` directories for the requested scope. Check optional directories for existence before listing them so missing user/project definition directories do not produce confusing `Path not found` messages.
190
+ 4. For new subagents, choose lowercase kebab-case names and clear trigger-focused descriptions. Write subagent markdown definitions in English by default; use another language only when the user explicitly requests it. Prefer `subagents` for new definitions unless the user explicitly needs compatibility with an `agents` harness.
187
191
  5. Set minimal tool allowlists; remove any `subagent_*` entries.
188
- 6. Configure `model_profiles`, `default_model`, and `default_effort` in `subagents.json` only when the user wants explicit routing; do not put model routing in subagent markdown unless the user explicitly asks for per-file overrides. Explain that unconfigured fields inherit from the orchestrator.
192
+ 6. Configure `model_profiles` in the matching local/global `subagents.json` when the user wants explicit per-agent routing. Configure `default_model` and `default_effort` in effective `subagents.json` only when the user wants defaults. Do not put model routing in subagent markdown unless the user explicitly asks for per-file overrides. Explain that unconfigured fields inherit from the orchestrator.
189
193
  7. Configure `debug: true` only for temporary diagnostics; keep `debug: false` by default and remember logs are written under the executing project's `.pi` directory.
190
194
  8. Validate JSON syntax for settings/subagents config and frontmatter/body structure for markdown agents.
191
195
  9. When configuring OpenCode-mode history opening, prefer `history_panel_shortcut` with `ctrl+<letter>` or `ctrl+,` values and document any built-in shortcut conflicts.
@@ -200,7 +204,7 @@ Model/effort resolution order:
200
204
  Return:
201
205
 
202
206
  - Skill applied: `subagents-configuration`.
203
- - Scope/path configured or reviewed.
207
+ - Scope/path configured or reviewed, including whether definitions came from `agents` or `subagents`.
204
208
  - Package settings and subagents/config fields added, changed, or preserved.
205
209
  - Tool allowlist, system-prompt isolation, Context7 scope, memory-tool scope, debug logging, model/effort decisions, and inheritance behavior.
206
210
  - Runtime behavior explained when relevant: task vs background, automatic notifications, `/subagents`, `ctrl+o`, and `subagent_result`.
package/src/config.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import type { ModelRef, SubagentDefinition, SubagentModelProfile, SubagentModelProfiles, SubagentSessionResources, SubagentsConfig, SubagentUiMode, ThinkingEffort } from './types.js';
4
+ import type { ModelRef, SubagentDefinition, SubagentDefinitionScope, SubagentModelProfile, SubagentModelProfiles, SubagentSessionResources, SubagentsConfig, SubagentUiMode, ThinkingEffort } from './types.js';
5
5
 
6
6
  const DEFAULT_TOOLS = ['read', 'memory_context', 'memory_search', 'memory_recall', 'memory_get'];
7
7
  const DEFAULT_MAX_CONCURRENCY = 5;
@@ -155,7 +155,7 @@ function parseModelProfiles(value: unknown): SubagentModelProfiles {
155
155
  if (!isPlainObject(value)) return {};
156
156
  const profiles: SubagentModelProfiles = {};
157
157
  for (const [name, rawProfile] of Object.entries(value)) {
158
- const normalizedName = name.trim();
158
+ const normalizedName = name.trim().toLowerCase();
159
159
  if (!normalizedName) continue;
160
160
  const profile = parseModelProfile(rawProfile);
161
161
  if (profile) profiles[normalizedName] = profile;
@@ -163,16 +163,6 @@ function parseModelProfiles(value: unknown): SubagentModelProfiles {
163
163
  return profiles;
164
164
  }
165
165
 
166
- function mergeModelProfiles(globalRaw: unknown, projectRaw: unknown): SubagentModelProfiles {
167
- const globalProfiles = parseModelProfiles(globalRaw);
168
- const projectProfiles = parseModelProfiles(projectRaw);
169
- const merged: SubagentModelProfiles = { ...globalProfiles };
170
- for (const [name, projectProfile] of Object.entries(projectProfiles)) {
171
- merged[name] = { ...(merged[name] ?? {}), ...projectProfile };
172
- }
173
- return merged;
174
- }
175
-
176
166
  function serializeModelRef(model: ModelRef): string {
177
167
  return `${model.provider}/${model.id}`;
178
168
  }
@@ -188,10 +178,14 @@ export function readSubagentsConfig(cwd: string): SubagentsConfig {
188
178
  const globalRaw = readJson(subagentsConfigPath());
189
179
  const projectRaw = readJson(path.join(cwd, '.pi', 'subagents.json'));
190
180
  const raw = { ...globalRaw, ...projectRaw };
181
+ const globalModelProfiles = parseModelProfiles(globalRaw.model_profiles);
182
+ const projectModelProfiles = parseModelProfiles(projectRaw.model_profiles);
191
183
  return {
192
184
  default_model: parseModel(raw.default_model),
193
185
  default_effort: parseEffort(raw.default_effort ?? raw.default_thinking_level ?? raw.thinkingLevel),
194
- model_profiles: mergeModelProfiles(globalRaw.model_profiles, projectRaw.model_profiles),
186
+ model_profiles: { ...globalModelProfiles, ...projectModelProfiles },
187
+ global_model_profiles: globalModelProfiles,
188
+ project_model_profiles: projectModelProfiles,
195
189
  timeout_ms: positiveInteger(raw.timeout_ms, DEFAULT_TIMEOUT_MS),
196
190
  stall_timeout_ms: positiveInteger(raw.stall_timeout_ms, DEFAULT_STALL_TIMEOUT_MS),
197
191
  max_concurrency: positiveInteger(raw.max_concurrency, DEFAULT_MAX_CONCURRENCY),
@@ -205,8 +199,16 @@ export function readSubagentsConfig(cwd: string): SubagentsConfig {
205
199
  };
206
200
  }
207
201
 
208
- export function saveGlobalSubagentModelProfile(input: { agentName: string; profile: SubagentModelProfile; agentDir?: string }): void {
209
- const file = subagentsConfigPath(input.agentDir);
202
+ function projectSubagentsConfigPath(cwd: string): string {
203
+ return path.join(cwd, '.pi', 'subagents.json');
204
+ }
205
+
206
+ function subagentsConfigPathForScope(input: { scope?: SubagentDefinitionScope; cwd?: string; agentDir?: string }): string {
207
+ return input.scope === 'project' && input.cwd ? projectSubagentsConfigPath(input.cwd) : subagentsConfigPath(input.agentDir);
208
+ }
209
+
210
+ export function saveSubagentModelProfile(input: { agentName: string; profile: SubagentModelProfile; scope?: SubagentDefinitionScope; cwd?: string; agentDir?: string }): void {
211
+ const file = subagentsConfigPathForScope(input);
210
212
  const root = readJson(file);
211
213
  const writableRoot: Record<string, unknown> = isPlainObject(root) ? { ...root } : {};
212
214
  const modelProfiles = isPlainObject(writableRoot.model_profiles) ? { ...writableRoot.model_profiles } : {};
@@ -219,8 +221,12 @@ export function saveGlobalSubagentModelProfile(input: { agentName: string; profi
219
221
  fs.writeFileSync(file, `${JSON.stringify(writableRoot, null, 2)}\n`, 'utf8');
220
222
  }
221
223
 
222
- export function resetGlobalSubagentModelProfileField(input: { agentName: string; field: 'model' | 'effort'; agentDir?: string }): void {
223
- const file = subagentsConfigPath(input.agentDir);
224
+ export function saveGlobalSubagentModelProfile(input: { agentName: string; profile: SubagentModelProfile; agentDir?: string }): void {
225
+ saveSubagentModelProfile({ ...input, scope: 'global' });
226
+ }
227
+
228
+ export function resetSubagentModelProfileField(input: { agentName: string; field: 'model' | 'effort'; scope?: SubagentDefinitionScope; cwd?: string; agentDir?: string }): void {
229
+ const file = subagentsConfigPathForScope(input);
224
230
  const root = readJson(file);
225
231
  const writableRoot: Record<string, unknown> = isPlainObject(root) ? { ...root } : {};
226
232
  const modelProfiles = isPlainObject(writableRoot.model_profiles) ? { ...writableRoot.model_profiles } : {};
@@ -235,7 +241,11 @@ export function resetGlobalSubagentModelProfileField(input: { agentName: string;
235
241
  fs.writeFileSync(file, `${JSON.stringify(writableRoot, null, 2)}\n`, 'utf8');
236
242
  }
237
243
 
238
- function loadSubagentsFromDir(dir: string): SubagentDefinition[] {
244
+ export function resetGlobalSubagentModelProfileField(input: { agentName: string; field: 'model' | 'effort'; agentDir?: string }): void {
245
+ resetSubagentModelProfileField({ ...input, scope: 'global' });
246
+ }
247
+
248
+ function loadSubagentsFromDir(dir: string, scope: SubagentDefinitionScope): SubagentDefinition[] {
239
249
  if (!fs.existsSync(dir)) return [];
240
250
  return fs.readdirSync(dir)
241
251
  .filter((f) => f.endsWith('.md'))
@@ -246,14 +256,43 @@ function loadSubagentsFromDir(dir: string): SubagentDefinition[] {
246
256
  const name = String(data.name || path.basename(file, '.md')).trim().toLowerCase();
247
257
  const description = String(data.description || `${name} subagent`).trim();
248
258
  const tools = sanitizeTools(Array.isArray(data.tools) ? data.tools.map(String) : DEFAULT_TOOLS);
249
- return { name, description, filePath, instructions: body.trim(), model: parseModel(data.model), effort: parseEffort(data.effort ?? data.thinking_level ?? data.thinkingLevel), tools };
259
+ return { name, description, filePath, instructions: body.trim(), model: parseModel(data.model), effort: parseEffort(data.effort ?? data.thinking_level ?? data.thinkingLevel), tools, scope };
250
260
  });
251
261
  }
252
262
 
263
+ function agentsDirSources(cwd: string): Array<{ scope: 'global' | 'project'; kind: 'agents' | 'subagents'; dir: string }> {
264
+ const globalAgentDir = agentDir();
265
+ return [
266
+ { scope: 'global', kind: 'agents', dir: path.join(globalAgentDir, 'agents') },
267
+ { scope: 'global', kind: 'subagents', dir: path.join(globalAgentDir, 'subagents') },
268
+ { scope: 'project', kind: 'agents', dir: path.join(cwd, '.pi', 'agents') },
269
+ { scope: 'project', kind: 'subagents', dir: path.join(cwd, '.pi', 'subagents') },
270
+ ];
271
+ }
272
+
273
+ export function subagentSourceWarnings(cwd: string): string[] {
274
+ const warnings: string[] = [];
275
+ for (const scope of ['global', 'project'] as const) {
276
+ const sources = agentsDirSources(cwd).filter((source) => source.scope === scope);
277
+ const agentsSource = sources.find((source) => source.kind === 'agents')!;
278
+ const subagentsSource = sources.find((source) => source.kind === 'subagents')!;
279
+ const agents = loadSubagentsFromDir(agentsSource.dir, agentsSource.scope);
280
+ const subagents = loadSubagentsFromDir(subagentsSource.dir, subagentsSource.scope);
281
+ const subagentNames = new Map(subagents.map((definition) => [definition.name, definition]));
282
+ for (const agent of agents) {
283
+ const subagent = subagentNames.get(agent.name);
284
+ if (!subagent) continue;
285
+ warnings.push(`Duplicate subagent name "${agent.name}" found in ${scope} agents and subagents directories; using subagents definition (${subagent.filePath}).`);
286
+ }
287
+ }
288
+ return warnings;
289
+ }
290
+
253
291
  export function loadSubagents(cwd: string): SubagentDefinition[] {
254
292
  const byName = new Map<string, SubagentDefinition>();
255
- for (const agent of loadSubagentsFromDir(path.join(agentDir(), 'subagents'))) byName.set(agent.name, agent);
256
- for (const agent of loadSubagentsFromDir(path.join(cwd, '.pi', 'subagents'))) byName.set(agent.name, agent);
293
+ for (const source of agentsDirSources(cwd)) {
294
+ for (const agent of loadSubagentsFromDir(source.dir, source.scope)) byName.set(agent.name, agent);
295
+ }
257
296
  return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
258
297
  }
259
298
 
@@ -1,8 +1,8 @@
1
1
  import os from 'node:os';
2
2
  import path from 'node:path';
3
- import { loadSubagents, readSubagentsConfig, resetGlobalSubagentModelProfileField, saveGlobalSubagentModelProfile } from './config.js';
3
+ import { loadSubagents, readSubagentsConfig, resetSubagentModelProfileField, saveSubagentModelProfile } from './config.js';
4
4
  import { resolveEffectiveSubagentProfile } from './profile-resolver.js';
5
- import type { ModelRef, SubagentDefinition, SubagentModelProfile, SubagentModelProfiles, SubagentsConfig, ThinkingEffort } from './types.js';
5
+ import type { ModelRef, SubagentDefinition, SubagentDefinitionScope, SubagentModelProfile, SubagentModelProfiles, SubagentsConfig, ThinkingEffort } from './types.js';
6
6
 
7
7
  export const KNOWN_SDD_PHASES = [
8
8
  'sdd-explore',
@@ -28,12 +28,17 @@ export type ModelProfileRow = {
28
28
  effectiveModel?: ModelRef;
29
29
  effectiveEffort?: ThinkingEffort;
30
30
  explicitProfile: SubagentModelProfile;
31
+ scope?: SubagentDefinitionScope;
31
32
  };
32
33
 
33
34
  export function globalSubagentsConfigPath(agentDir = path.join(os.homedir(), '.pi', 'agent')): string {
34
35
  return path.join(agentDir, 'subagents.json');
35
36
  }
36
37
 
38
+ export function projectSubagentsConfigPath(cwd: string): string {
39
+ return path.join(cwd, '.pi', 'subagents.json');
40
+ }
41
+
37
42
  function modelKey(model: ModelRef): string {
38
43
  return `${model.provider}/${model.id}`;
39
44
  }
@@ -101,7 +106,8 @@ export function buildModelProfileRows(input: {
101
106
  effortLabel: resolved.effort.label,
102
107
  effectiveModel: resolved.model.value,
103
108
  effectiveEffort: resolved.effort.value,
104
- explicitProfile: { ...(input.config.model_profiles[definition.name] ?? {}) },
109
+ explicitProfile: { ...((definition.scope === 'project' ? input.config.project_model_profiles?.[definition.name] : input.config.global_model_profiles?.[definition.name]) ?? {}) },
110
+ scope: definition.scope ?? 'global',
105
111
  };
106
112
  });
107
113
  }
@@ -156,16 +162,25 @@ export function applyDirtyProfileEdit(input: {
156
162
  return nextDirtyProfiles;
157
163
  }
158
164
 
159
- export function commitStagedModelProfiles(input: { stagedProfiles: SubagentModelProfiles; save: boolean; agentDir?: string }): string {
160
- if (!input.save) return `Cancelled. No changes written to ${globalSubagentsConfigPath(input.agentDir)}.`;
165
+ export function commitStagedModelProfiles(input: { stagedProfiles: SubagentModelProfiles; save: boolean; agentDir?: string; cwd?: string; profileScopes?: Record<string, SubagentDefinitionScope> }): string {
166
+ const globalPath = globalSubagentsConfigPath(input.agentDir);
167
+ const projectPath = input.cwd ? projectSubagentsConfigPath(input.cwd) : undefined;
168
+ if (!input.save) return `Cancelled. No changes written to ${globalPath}.`;
169
+ const touched = new Set<string>();
161
170
  for (const [agentName, profile] of Object.entries(input.stagedProfiles)) {
162
- if (profile.model || profile.effort) saveGlobalSubagentModelProfile({ agentName, profile, agentDir: input.agentDir });
171
+ const scope = input.profileScopes?.[agentName.trim().toLowerCase()] ?? 'global';
172
+ const target = scope === 'project' && input.cwd ? projectPath! : globalPath;
173
+ touched.add(target);
174
+ if (profile.model || profile.effort) saveSubagentModelProfile({ agentName, profile, scope, cwd: input.cwd, agentDir: input.agentDir });
163
175
  else {
164
- resetGlobalSubagentModelProfileField({ agentName, field: 'model', agentDir: input.agentDir });
165
- resetGlobalSubagentModelProfileField({ agentName, field: 'effort', agentDir: input.agentDir });
176
+ resetSubagentModelProfileField({ agentName, field: 'model', scope, cwd: input.cwd, agentDir: input.agentDir });
177
+ resetSubagentModelProfileField({ agentName, field: 'effort', scope, cwd: input.cwd, agentDir: input.agentDir });
166
178
  }
167
179
  }
168
- return `Saved subagent model profiles to ${globalSubagentsConfigPath(input.agentDir)}.`;
180
+ const targets = [...touched];
181
+ return targets.length > 1
182
+ ? `Saved subagent model profiles to ${targets.join(' and ')}.`
183
+ : `Saved subagent model profiles to ${targets[0] ?? globalPath}.`;
169
184
  }
170
185
 
171
186
  export function buildNoChangesModelProfilesMessage(agentDir?: string): string {
@@ -188,6 +203,7 @@ type ModalInput = {
188
203
  rows: ModelProfileRow[];
189
204
  availableModels?: any[];
190
205
  tui?: { requestRender?: () => void };
206
+ theme?: any;
191
207
  done: (result: SubagentModelProfilesModalResult) => void;
192
208
  };
193
209
 
@@ -322,6 +338,9 @@ export function createSubagentModelProfilesModal(input: ModalInput): ModalCompon
322
338
  };
323
339
 
324
340
  const rowKey = (row: ModelProfileRow): string => row.name.trim().toLowerCase();
341
+ const rowScope = (row: ModelProfileRow): SubagentDefinitionScope => row.scope ?? 'global';
342
+ const dim = (text: string): string => input.theme?.fg?.('dim', text) ?? text;
343
+ const scopedName = (row: ModelProfileRow): string => `${row.name} ${dim(rowScope(row) === 'project' ? '(local)' : '(global)')}`;
325
344
  const hasDirtyProfileFor = (row: ModelProfileRow): boolean => Object.prototype.hasOwnProperty.call(dirtyProfiles, rowKey(row));
326
345
  const dirtyProfileFor = (row: ModelProfileRow): SubagentModelProfile | undefined => dirtyProfiles[rowKey(row)];
327
346
 
@@ -351,16 +370,16 @@ export function createSubagentModelProfilesModal(input: ModalInput): ModalCompon
351
370
  const rowListLines = (width: number): string[] => {
352
371
  const innerWidth = Math.max(1, Math.floor(width || 1) - 2);
353
372
  const visibleRows = rows.slice(scrollOffset, scrollOffset + 10);
354
- if (innerWidth >= 92) {
355
- const nameWidth = 24;
356
- const effortWidth = 22;
357
- const modelWidth = Math.max(18, innerWidth - nameWidth - effortWidth - 6);
373
+ if (innerWidth >= 100) {
374
+ const nameWidth = 32;
375
+ const effortWidth = 24;
376
+ const modelWidth = Math.max(24, innerWidth - nameWidth - effortWidth - 6);
358
377
  const lines = [`${padToVisibleWidth('agent/phase', nameWidth)} ${padToVisibleWidth('model', modelWidth)} ${padToVisibleWidth('effort', effortWidth)}`];
359
378
  for (const [offset, item] of visibleRows.entries()) {
360
379
  const index = scrollOffset + offset;
361
380
  const marker = index === selectedIndex ? '›' : ' ';
362
381
  const dirty = hasDirtyProfileFor(item) ? '*' : ' ';
363
- lines.push(`${marker} ${dirty} ${padToVisibleWidth(item.name, nameWidth - 4)} ${padToVisibleWidth(rowModelText(item), modelWidth)} ${padToVisibleWidth(rowEffortText(item), effortWidth)}`);
382
+ lines.push(`${marker} ${dirty} ${padToVisibleWidth(scopedName(item), nameWidth - 4)} ${padToVisibleWidth(rowModelText(item), modelWidth)} ${padToVisibleWidth(rowEffortText(item), effortWidth)}`);
364
383
  }
365
384
  return lines;
366
385
  }
@@ -369,7 +388,7 @@ export function createSubagentModelProfilesModal(input: ModalInput): ModalCompon
369
388
  const index = scrollOffset + offset;
370
389
  const marker = index === selectedIndex ? '›' : ' ';
371
390
  const dirty = hasDirtyProfileFor(item) ? '*' : ' ';
372
- lines.push(`${marker} ${dirty} ${item.name} · ${rowModelText(item)} · ${rowEffortText(item)}`);
391
+ lines.push(`${marker} ${dirty} ${scopedName(item)} · ${rowModelText(item)} · ${rowEffortText(item)}`);
373
392
  }
374
393
  return lines;
375
394
  };
@@ -377,7 +396,7 @@ export function createSubagentModelProfilesModal(input: ModalInput): ModalCompon
377
396
  const renderMain = (width: number): string[] => {
378
397
  const dirtyCount = Object.keys(dirtyProfiles).length;
379
398
  const body = [
380
- `target: global · ${pendingLabel(dirtyCount)}`,
399
+ `target: local/global by subagent scope · ${pendingLabel(dirtyCount)}`,
381
400
  '↑/↓/j/k move · enter/m model · e effort · M/E/r reset · s save · esc/q cancel',
382
401
  '',
383
402
  ...rowListLines(width),
@@ -513,7 +532,8 @@ export function buildNonTuiModelProfilesMessage(agentDir?: string): string {
513
532
  }
514
533
 
515
534
  function rowChoice(row: ModelProfileRow): string {
516
- return `${row.name} model ${row.modelLabel}; effort ${row.effortLabel}`;
535
+ const scope = row.scope === 'project' ? 'local' : 'global';
536
+ return `${row.name} (${scope}) — model ${row.modelLabel}; effort ${row.effortLabel}`;
517
537
  }
518
538
 
519
539
  async function getAvailableModels(ctx: any): Promise<any[]> {
@@ -525,9 +545,9 @@ async function getAvailableModels(ctx: any): Promise<any[]> {
525
545
  }
526
546
  }
527
547
 
528
- async function chooseSave(ctx: any, stagedProfiles: SubagentModelProfiles, agentDir?: string): Promise<string> {
548
+ async function chooseSave(ctx: any, stagedProfiles: SubagentModelProfiles, input: { agentDir?: string; cwd?: string; profileScopes?: Record<string, SubagentDefinitionScope> } = {}): Promise<string> {
529
549
  const decision = await ctx.ui.select('Save subagent model profile changes?', ['Save', 'Cancel']);
530
- const message = commitStagedModelProfiles({ stagedProfiles, save: decision === 'Save', agentDir });
550
+ const message = commitStagedModelProfiles({ stagedProfiles, save: decision === 'Save', agentDir: input.agentDir, cwd: input.cwd, profileScopes: input.profileScopes });
531
551
  ctx.ui.notify?.(message, decision === 'Save' ? 'info' : 'warning');
532
552
  return message;
533
553
  }
@@ -543,22 +563,24 @@ export async function runSubagentModelsCommand(ctx: any = {}): Promise<string> {
543
563
  const config = readSubagentsConfig(cwd);
544
564
  const availableModels = await getAvailableModels(ctx);
545
565
  const rows = buildModelProfileRows({ definitions, config, ctx, availableModels });
566
+ const profileScopes: Record<string, SubagentDefinitionScope> = Object.fromEntries(rows.map((row) => [row.name.trim().toLowerCase(), row.scope ?? 'global']));
546
567
 
547
568
  if (hasCustomUi) {
548
569
  const result = await ctx.ui.custom(
549
- (tui: any, _theme: any, _keybindings: any, done: (result: SubagentModelProfilesModalResult) => void) => createSubagentModelProfilesModal({
570
+ (tui: any, theme: any, _keybindings: any, done: (result: SubagentModelProfilesModalResult) => void) => createSubagentModelProfilesModal({
550
571
  rows,
551
572
  availableModels,
552
573
  tui,
574
+ theme,
553
575
  done,
554
576
  }),
555
- { overlay: true, overlayOptions: { anchor: 'center', width: '90%', maxHeight: '85%', minWidth: 74 } },
577
+ { overlay: true, overlayOptions: { anchor: 'center', width: '96%', maxHeight: '90%', minWidth: 96 } },
556
578
  ) as SubagentModelProfilesModalResult | undefined;
557
579
 
558
580
  if (result?.action === 'save') {
559
581
  const hasDirtyRows = Object.keys(result.dirtyProfiles).length > 0;
560
582
  const message = hasDirtyRows
561
- ? commitStagedModelProfiles({ stagedProfiles: result.dirtyProfiles, save: true, agentDir })
583
+ ? commitStagedModelProfiles({ stagedProfiles: result.dirtyProfiles, save: true, agentDir, cwd, profileScopes })
562
584
  : buildNoChangesModelProfilesMessage(agentDir);
563
585
  ctx.ui.notify?.(message, 'info');
564
586
  return message;
@@ -605,5 +627,5 @@ export async function runSubagentModelsCommand(ctx: any = {}): Promise<string> {
605
627
  else staged = stageModelProfileEdit(staged, { agentName: row.name, effort });
606
628
  }
607
629
 
608
- return chooseSave(ctx, staged, agentDir);
630
+ return chooseSave(ctx, staged, { agentDir, cwd, profileScopes });
609
631
  }
@@ -25,8 +25,13 @@ function field<T>(source: ProfileValueSource, value: T | undefined, format: (val
25
25
  return { value, source, label: profileSourceLabel(source, value, format) };
26
26
  }
27
27
 
28
+ function profileForDefinition(definition: SubagentDefinition, config: SubagentsConfig) {
29
+ if (definition.scope === 'project') return config.project_model_profiles?.[definition.name] ?? (config.project_model_profiles ? undefined : config.model_profiles[definition.name]);
30
+ return config.global_model_profiles?.[definition.name] ?? (config.global_model_profiles ? undefined : config.model_profiles[definition.name]);
31
+ }
32
+
28
33
  function resolveModel(definition: SubagentDefinition, config: SubagentsConfig, ctx: any): ResolvedProfileField<ModelRef> {
29
- const profile = config.model_profiles[definition.name];
34
+ const profile = profileForDefinition(definition, config);
30
35
  if (profile?.model) return field('profile', profile.model, modelLabel);
31
36
  if (definition.model) return field('definition', definition.model, modelLabel);
32
37
  if (config.default_model) return field('default', config.default_model, modelLabel);
@@ -36,7 +41,7 @@ function resolveModel(definition: SubagentDefinition, config: SubagentsConfig, c
36
41
  }
37
42
 
38
43
  function resolveEffort(definition: SubagentDefinition, config: SubagentsConfig, ctx: any): ResolvedProfileField<ThinkingEffort> {
39
- const profile = config.model_profiles[definition.name];
44
+ const profile = profileForDefinition(definition, config);
40
45
  if (profile?.effort) return field('profile', profile.effort, String);
41
46
  if (definition.effort) return field('definition', definition.effort, String);
42
47
  if (config.default_effort) return field('default', config.default_effort, String);
package/src/tools.ts CHANGED
@@ -299,7 +299,7 @@ export function registerSubagentTools(pi: any, manager: SubagentManager): void {
299
299
  name: 'subagent_list_agents',
300
300
  label: 'Subagent List Agents',
301
301
  description: 'List available markdown-defined subagents for delegation.',
302
- promptSnippet: 'List available subagents loaded from .pi/subagents/*.md.',
302
+ promptSnippet: 'List available subagents loaded from global/project agents and subagents markdown directories.',
303
303
  parameters: Type.Object({}),
304
304
  async execute(_id: string, _params: any, _signal: any, _onUpdate: any, ctx: any) {
305
305
  try { const agents = manager.listAgents(ctx?.cwd ?? process.cwd()); return ok(`Found ${agents.length} subagent(s).`, { agents }); } catch (e) { return fail(e); }
package/src/types.ts CHANGED
@@ -12,6 +12,7 @@ export type SubagentModelProfile = {
12
12
  };
13
13
 
14
14
  export type SubagentModelProfiles = Record<string, SubagentModelProfile>;
15
+ export type SubagentDefinitionScope = 'global' | 'project';
15
16
 
16
17
  export type ProfileValueSource = 'profile' | 'definition' | 'default' | 'orchestrator' | 'unresolved';
17
18
 
@@ -35,6 +36,7 @@ export type SubagentDefinition = {
35
36
  model?: ModelRef;
36
37
  effort?: ThinkingEffort;
37
38
  tools: string[];
39
+ scope?: SubagentDefinitionScope;
38
40
  };
39
41
 
40
42
  export type SubagentSessionResources = 'full' | 'lean';
@@ -44,6 +46,8 @@ export type SubagentsConfig = {
44
46
  default_model?: ModelRef;
45
47
  default_effort?: ThinkingEffort;
46
48
  model_profiles: SubagentModelProfiles;
49
+ global_model_profiles?: SubagentModelProfiles;
50
+ project_model_profiles?: SubagentModelProfiles;
47
51
  timeout_ms: number;
48
52
  stall_timeout_ms: number;
49
53
  max_concurrency: number;
package/src/ui.ts CHANGED
@@ -214,15 +214,37 @@ export class SubagentsHistoryPanel {
214
214
 
215
215
  private taskStrip(width: number): string {
216
216
  const tasks = this.tasks();
217
- const max = Math.max(1, Math.min(tasks.length, 8));
218
- const start = Math.min(Math.max(0, this.selected - Math.floor(max / 2)), Math.max(0, tasks.length - max));
219
- const chips: string[] = [];
220
- for (let i = start; i < Math.min(tasks.length, start + max); i++) {
221
- const task = tasks[i]!;
222
- const label = `${i === this.selected ? '●' : '○'} ${task.agent}:${task.status}${task.effort ? ` effort:${task.effort}` : ''}`;
223
- chips.push(i === this.selected ? (this.theme?.fg?.('accent', label) ?? label) : (this.theme?.fg?.('dim', label) ?? label));
217
+ const dim = (s: string) => this.theme?.fg?.('dim', s) ?? s;
218
+ const selected = (s: string) => this.theme?.fg?.('warning', s) ?? s;
219
+ const chip = (index: number): { raw: string; styled: string } => {
220
+ const task = tasks[index]!;
221
+ const raw = `${index === this.selected ? '●' : '○'} ${task.agent}:${task.status}${task.effort ? ` effort:${task.effort}` : ''}`;
222
+ return { raw, styled: index === this.selected ? selected(raw) : dim(raw) };
223
+ };
224
+ const selectedChip = chip(this.selected);
225
+ let start = this.selected;
226
+ let end = this.selected + 1;
227
+ let raw = selectedChip.raw;
228
+ while (start > 0 || end < tasks.length) {
229
+ const preferLeft = this.selected - start <= end - this.selected - 1;
230
+ const nextIndex = preferLeft && start > 0 ? start - 1 : end < tasks.length ? end : start > 0 ? start - 1 : -1;
231
+ if (nextIndex < 0) break;
232
+ const next = chip(nextIndex).raw;
233
+ const candidate = nextIndex < start ? `${next} ${raw}` : `${raw} ${next}`;
234
+ const prefix = `executions ${Math.min(start, nextIndex) + 1}-${Math.max(end, nextIndex + 1)}/${tasks.length} `;
235
+ const leftIndicator = Math.min(start, nextIndex) > 0 ? '‹ ' : '';
236
+ const rightIndicator = Math.max(end, nextIndex + 1) < tasks.length ? ' ›' : '';
237
+ if (this.visibleWidth(`${prefix}${leftIndicator}${candidate}${rightIndicator}`) > width) break;
238
+ raw = candidate;
239
+ start = Math.min(start, nextIndex);
240
+ end = Math.max(end, nextIndex + 1);
224
241
  }
225
- return this.truncateToWidth(chips.join(' '), width);
242
+ const styledChips: string[] = [];
243
+ for (let i = start; i < end; i++) styledChips.push(chip(i).styled);
244
+ const prefix = dim(`executions ${start + 1}-${end}/${tasks.length}`);
245
+ const leftIndicator = start > 0 ? `${dim('‹')} ` : '';
246
+ const rightIndicator = end < tasks.length ? ` ${dim('›')}` : '';
247
+ return `${prefix} ${leftIndicator}${styledChips.join(' ')}${rightIndicator}`;
226
248
  }
227
249
 
228
250
  private tasks(): SubagentTask[] {