pi-subagents-j0k3r 1.1.0 → 1.1.2

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ### Fixed
6
+ - Fixed manual task-mode background handoff so it frees the chat only when the user explicitly sends the running subagent to background.
7
+ - Fixed background completion delivery so notifications arrive while the main agent continues working without triggering an extra follow-up turn.
8
+
3
9
  ## 1.1.0 - 2026-06-27
4
10
 
5
11
  ### Added
package/README.md CHANGED
@@ -74,6 +74,8 @@ Load order:
74
74
 
75
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
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.
78
+
77
79
  Default global agent directory:
78
80
 
79
81
  ```txt
@@ -129,7 +131,7 @@ Config files:
129
131
  .pi/subagents.json # project
130
132
  ```
131
133
 
132
- 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 the explicit exception: per-agent model/effort routing is read only from the global `~/.pi/agent/subagents.json` or `$PI_CODING_AGENT_DIR/subagents.json`, and project-local `model_profiles` are ignored.
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.
133
135
 
134
136
  Example:
135
137
 
@@ -170,7 +172,7 @@ Example:
170
172
  |---|---:|---|
171
173
  | `default_model` | current orchestrator model | Fallback model for all subagents. Format: `provider/model-id`. |
172
174
  | `default_effort` | current orchestrator effort | Fallback thinking effort. Also accepts `default_thinking_level` or `thinkingLevel`. |
173
- | `model_profiles` | `{}` | Global-only per-agent model/effort overrides. Project-local `.pi/subagents.json` `model_profiles` are ignored. |
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. |
174
176
  | `timeout_ms` | `600000` | Total timeout per subagent task. |
175
177
  | `stall_timeout_ms` | `120000` | Inactivity timeout for a subagent session. |
176
178
  | `max_concurrency` | `5` | Max concurrent subagent tasks per cwd/config pair. |
@@ -202,7 +204,7 @@ any tool starting with subagent_
202
204
 
203
205
  Effective model resolution order:
204
206
 
205
- 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
206
208
  2. subagent frontmatter `model`
207
209
  3. `default_model`
208
210
  4. current orchestrator model
@@ -210,7 +212,7 @@ Effective model resolution order:
210
212
 
211
213
  Effective effort resolution order:
212
214
 
213
- 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
214
216
  2. subagent frontmatter `effort` / `thinking_level` / `thinkingLevel`
215
217
  3. `default_effort`
216
218
  4. current orchestrator thinking level
@@ -284,20 +286,14 @@ Behavior:
284
286
  | Entry point | Description |
285
287
  |---|---|
286
288
  | `/subagents` | Open the session-focused TUI subagent history panel. |
287
- | `/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. |
288
290
  | `ctrl+,` | Open the TUI subagent history panel in OpenCode mode by default. Configurable via `history_panel_shortcut` in `subagents.json`. |
289
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`. |
290
292
  | `ctrl+h` | Send the running Claude-mode subagent task to the background by default. Configurable via `background_handoff_shortcut` in `subagents.json`. |
291
293
 
292
- `/subagent-models` writes global profile changes to:
293
-
294
- ```txt
295
- ~/.pi/agent/subagents.json
296
- ```
297
-
298
- 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.
299
295
 
300
- 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.
301
297
 
302
298
  ## Task history
303
299
 
@@ -394,7 +390,7 @@ This package bundles:
394
390
  - `index.ts` and `src/**` — the Pi extension runtime.
395
391
  - `skills/subagents-configuration/SKILL.md` — configuration guidance for agents that need to create or edit subagent definitions.
396
392
 
397
- 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`.
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.
398
394
 
399
395
  ## Development
400
396
 
package/index.ts CHANGED
@@ -290,6 +290,31 @@ export function completionMessage(task: any): string {
290
290
  ].join('\n');
291
291
  }
292
292
 
293
+ export function sendSubagentCompletionMessage(pi: any, task: any): void {
294
+ pi.sendMessage?.({
295
+ customType: 'subagent-completion',
296
+ content: completionMessage(task),
297
+ display: true,
298
+ details: {
299
+ full_result: task.result ?? task.error ?? task.output_preview,
300
+ task: {
301
+ id: task.id,
302
+ agent: task.agent,
303
+ status: task.status,
304
+ mode: task.mode,
305
+ model: task.model,
306
+ effort: task.effort,
307
+ usage: task.usage,
308
+ result: task.result,
309
+ error: task.error,
310
+ },
311
+ },
312
+ }, {
313
+ triggerTurn: false,
314
+ deliverAs: 'steer',
315
+ });
316
+ }
317
+
293
318
  export function renderSubagentCompletionMessage(message: any, options: any, theme: any) {
294
319
  const details = message.details ?? {};
295
320
  const task = details.task ?? details;
@@ -336,28 +361,7 @@ export function renderSubagentCompletionMessage(message: any, options: any, them
336
361
  export default function subagentsExtension(pi: any): void {
337
362
  pi.registerMessageRenderer?.('subagent-completion', renderSubagentCompletionMessage);
338
363
  const manager = new SubagentManager(undefined, undefined, (task) => {
339
- pi.sendMessage?.({
340
- customType: 'subagent-completion',
341
- content: completionMessage(task),
342
- display: true,
343
- details: {
344
- full_result: task.result ?? task.error ?? task.output_preview,
345
- task: {
346
- id: task.id,
347
- agent: task.agent,
348
- status: task.status,
349
- mode: task.mode,
350
- model: task.model,
351
- effort: task.effort,
352
- usage: task.usage,
353
- result: task.result,
354
- error: task.error,
355
- },
356
- },
357
- }, {
358
- triggerTurn: true,
359
- deliverAs: 'followUp',
360
- });
364
+ sendSubagentCompletionMessage(pi, task);
361
365
  });
362
366
  registerSubagentTools(pi, manager);
363
367
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents-j0k3r",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
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",
@@ -92,10 +92,11 @@ Do not load this skill for ordinary subagent delegation/use (`subagent_run`, tas
92
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.
93
93
  - For SDD phase agents, memory write tools may be allowed only for active SDD flow memory/artifacts according to `sdd-workflow`.
94
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" })`.
95
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.
96
- - 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, with the explicit exception that `model_profiles` are global-only.
97
- - `model_profiles` are read only from global `$PI_CODING_AGENT_DIR/subagents.json` or `~/.pi/agent/subagents.json`; project-local `.pi/subagents.json` must not set or override subagent model/effort routing.
98
- - Prefer configuring subagent `model` and `effort` in global `subagents.json` under `model_profiles`, not in project-local config or in the subagent markdown frontmatter. Markdown definitions should usually contain identity, description, tool allowlist, and behavioral instructions only.
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.
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.
99
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.
100
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.
101
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.
@@ -154,7 +155,7 @@ tools:
154
155
  Instructions...
155
156
  ```
156
157
 
157
- Configure model/effort routing separately in the global `subagents.json` when needed. If no global 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.
158
159
 
159
160
  ```json
160
161
  {
@@ -169,7 +170,7 @@ Configure model/effort routing separately in the global `subagents.json` when ne
169
170
 
170
171
  Model/effort resolution order:
171
172
 
172
- 1. Global `model_profiles[agentName]` in `$PI_CODING_AGENT_DIR/subagents.json` or `~/.pi/agent/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.
173
174
  2. Markdown frontmatter `model` / `effort` only for explicit per-file overrides.
174
175
  3. `default_model` / `default_effort` from effective `subagents.json` config.
175
176
  4. Current orchestrator model / effort.
@@ -179,16 +180,16 @@ Model/effort resolution order:
179
180
  - If the subagent will modify files, run bash, or write memory, ask whether a full SDD workflow or stricter review is required.
180
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.
181
182
  - If a project wants many subagents or broad tools, recommend starting with read-only discovery agents and expanding deliberately.
182
- - If the user asks for project-local model profiles, explain that `model_profiles` are global-only and ask whether they want to update the global config instead.
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.
183
184
 
184
185
  ## Execution Steps
185
186
 
186
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.
187
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.
188
- 3. Read existing subagent markdown/config before editing, checking both `agents` and `subagents` directories for the requested scope.
189
- 4. For new subagents, choose lowercase kebab-case names and clear trigger-focused descriptions. Prefer `subagents` for new definitions unless the user explicitly needs compatibility with an `agents` harness.
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.
190
191
  5. Set minimal tool allowlists; remove any `subagent_*` entries.
191
- 6. Configure `model_profiles` only in global `subagents.json` when the user wants explicit per-agent routing; project-local `model_profiles` are ignored. 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.
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.
192
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.
193
194
  8. Validate JSON syntax for settings/subagents config and frontmatter/body structure for markdown agents.
194
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.
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;
@@ -178,10 +178,14 @@ export function readSubagentsConfig(cwd: string): SubagentsConfig {
178
178
  const globalRaw = readJson(subagentsConfigPath());
179
179
  const projectRaw = readJson(path.join(cwd, '.pi', 'subagents.json'));
180
180
  const raw = { ...globalRaw, ...projectRaw };
181
+ const globalModelProfiles = parseModelProfiles(globalRaw.model_profiles);
182
+ const projectModelProfiles = parseModelProfiles(projectRaw.model_profiles);
181
183
  return {
182
184
  default_model: parseModel(raw.default_model),
183
185
  default_effort: parseEffort(raw.default_effort ?? raw.default_thinking_level ?? raw.thinkingLevel),
184
- model_profiles: parseModelProfiles(globalRaw.model_profiles),
186
+ model_profiles: { ...globalModelProfiles, ...projectModelProfiles },
187
+ global_model_profiles: globalModelProfiles,
188
+ project_model_profiles: projectModelProfiles,
185
189
  timeout_ms: positiveInteger(raw.timeout_ms, DEFAULT_TIMEOUT_MS),
186
190
  stall_timeout_ms: positiveInteger(raw.stall_timeout_ms, DEFAULT_STALL_TIMEOUT_MS),
187
191
  max_concurrency: positiveInteger(raw.max_concurrency, DEFAULT_MAX_CONCURRENCY),
@@ -195,8 +199,16 @@ export function readSubagentsConfig(cwd: string): SubagentsConfig {
195
199
  };
196
200
  }
197
201
 
198
- export function saveGlobalSubagentModelProfile(input: { agentName: string; profile: SubagentModelProfile; agentDir?: string }): void {
199
- 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);
200
212
  const root = readJson(file);
201
213
  const writableRoot: Record<string, unknown> = isPlainObject(root) ? { ...root } : {};
202
214
  const modelProfiles = isPlainObject(writableRoot.model_profiles) ? { ...writableRoot.model_profiles } : {};
@@ -209,8 +221,12 @@ export function saveGlobalSubagentModelProfile(input: { agentName: string; profi
209
221
  fs.writeFileSync(file, `${JSON.stringify(writableRoot, null, 2)}\n`, 'utf8');
210
222
  }
211
223
 
212
- export function resetGlobalSubagentModelProfileField(input: { agentName: string; field: 'model' | 'effort'; agentDir?: string }): void {
213
- 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);
214
230
  const root = readJson(file);
215
231
  const writableRoot: Record<string, unknown> = isPlainObject(root) ? { ...root } : {};
216
232
  const modelProfiles = isPlainObject(writableRoot.model_profiles) ? { ...writableRoot.model_profiles } : {};
@@ -225,7 +241,11 @@ export function resetGlobalSubagentModelProfileField(input: { agentName: string;
225
241
  fs.writeFileSync(file, `${JSON.stringify(writableRoot, null, 2)}\n`, 'utf8');
226
242
  }
227
243
 
228
- 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[] {
229
249
  if (!fs.existsSync(dir)) return [];
230
250
  return fs.readdirSync(dir)
231
251
  .filter((f) => f.endsWith('.md'))
@@ -236,7 +256,7 @@ function loadSubagentsFromDir(dir: string): SubagentDefinition[] {
236
256
  const name = String(data.name || path.basename(file, '.md')).trim().toLowerCase();
237
257
  const description = String(data.description || `${name} subagent`).trim();
238
258
  const tools = sanitizeTools(Array.isArray(data.tools) ? data.tools.map(String) : DEFAULT_TOOLS);
239
- 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 };
240
260
  });
241
261
  }
242
262
 
@@ -254,8 +274,10 @@ export function subagentSourceWarnings(cwd: string): string[] {
254
274
  const warnings: string[] = [];
255
275
  for (const scope of ['global', 'project'] as const) {
256
276
  const sources = agentsDirSources(cwd).filter((source) => source.scope === scope);
257
- const agents = loadSubagentsFromDir(sources.find((source) => source.kind === 'agents')!.dir);
258
- const subagents = loadSubagentsFromDir(sources.find((source) => source.kind === 'subagents')!.dir);
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);
259
281
  const subagentNames = new Map(subagents.map((definition) => [definition.name, definition]));
260
282
  for (const agent of agents) {
261
283
  const subagent = subagentNames.get(agent.name);
@@ -269,7 +291,7 @@ export function subagentSourceWarnings(cwd: string): string[] {
269
291
  export function loadSubagents(cwd: string): SubagentDefinition[] {
270
292
  const byName = new Map<string, SubagentDefinition>();
271
293
  for (const source of agentsDirSources(cwd)) {
272
- for (const agent of loadSubagentsFromDir(source.dir)) byName.set(agent.name, agent);
294
+ for (const agent of loadSubagentsFromDir(source.dir, source.scope)) byName.set(agent.name, agent);
273
295
  }
274
296
  return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
275
297
  }
@@ -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
@@ -357,7 +357,8 @@ export function registerSubagentTools(pi: any, manager: SubagentManager): void {
357
357
  if (cancelledByDoubleEscape) throw new Error('Subagent run cancelled by double escape');
358
358
  if (!('results' in result)) {
359
359
  const details = compactResultDetails(result as any);
360
- return ok(backgroundLaunchContent(result.task_ids, 'Sent'), details);
360
+ const response = ok(backgroundLaunchContent(result.task_ids, 'Sent'), details);
361
+ return isBackground ? response : { ...response, terminate: true };
361
362
  }
362
363
  const failedTasks = (result.results ?? []).filter((task) => task.status === 'failed' || task.status === 'cancelled');
363
364
  const text = result.mode === 'background'
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[] {