@bermudi/pi-delegate 0.1.7 → 0.1.9

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/README.md CHANGED
@@ -38,6 +38,19 @@ Parent extension/MCP tools are not copied, and project instructions are rebuilt
38
38
  for the task's `cwd`. Omit `agent` when you want an ad-hoc task using delegate's
39
39
  normal inline defaults instead.
40
40
 
41
+ The other built-ins are:
42
+
43
+ - `scout` — read-only investigation with `read`, `grep`, `find`, and `ls`.
44
+ - `coder` — implementation and verification with `read`, `write`, `edit`, and
45
+ `bash` in the shared workspace.
46
+ - `reviewer` — review with `read` and `bash`, using a disposable scratch copy by
47
+ default. Set `workspace: "shared"` when a reviewer needs a persistent
48
+ `sessionId`.
49
+
50
+ Fresh built-ins inherit the parent's exact model object and thinking level.
51
+ Task-level overrides win; settings can provide unconditional overrides or exact
52
+ parent-model overrides under `delegate.agentOverridesByParentModel`.
53
+
41
54
  ### Disposable scratch workspace
42
55
 
43
56
  For review, tests, or other commands whose project changes should be thrown
@@ -136,7 +149,7 @@ over an installed extension.
136
149
  rare override. Markdown agents are examples of custom agents.
137
150
  - **Named agent** / **Markdown agent** — A reusable custom agent persisted as a
138
151
  Markdown file in `.pi/agents/*.md` or `~/.pi/agent/agents/*.md`. The frontmatter
139
- defines its name, description, model, tools, thinking level, and skills; the
152
+ defines its name, description, model, tools, and thinking level; the
140
153
  Markdown body is its system prompt.
141
154
  - **Ad-hoc subagent** — A subagent created from inline task fields instead of a
142
155
  named Markdown agent profile. In current output this is labeled `ad-hoc`.
package/agents.ts CHANGED
@@ -7,8 +7,13 @@ import type {
7
7
  } from "@earendil-works/pi-agent-core";
8
8
  import { parseFrontmatter as parsePiFrontmatter } from "@earendil-works/pi-coding-agent";
9
9
  import {
10
+ BUILTIN_AGENT_NAMES,
11
+ CODER_AGENT_NAME,
10
12
  DEFAULT_AGENT_NAME,
11
13
  DEFAULT_TOOLS,
14
+ READONLY_TOOLS,
15
+ REVIEWER_AGENT_NAME,
16
+ SCOUT_AGENT_NAME,
12
17
  VALID_THINKING,
13
18
  } from "./constants.ts";
14
19
  import { resolveToolGroups } from "./tools.ts";
@@ -110,6 +115,50 @@ export function parseFrontmatter(
110
115
 
111
116
  // ── Agent Discovery ───────────────────────────────────────────────────────
112
117
 
118
+ /** Built-in profiles are always available and cannot vary with Markdown files. */
119
+ export const BUILTIN_AGENT_CONFIGS: Readonly<Record<string, AgentConfig>> = {
120
+ [DEFAULT_AGENT_NAME]: {
121
+ name: DEFAULT_AGENT_NAME,
122
+ description:
123
+ "Mirror the live parent model, thinking level, native tools, and base prompt.",
124
+ tools: DEFAULT_TOOLS,
125
+ systemPrompt: "",
126
+ builtin: true,
127
+ workspace: "shared",
128
+ },
129
+ [SCOUT_AGENT_NAME]: {
130
+ name: SCOUT_AGENT_NAME,
131
+ description: "Investigate without modifying the source project.",
132
+ tools: READONLY_TOOLS,
133
+ systemPrompt:
134
+ "Explore the codebase to answer the assigned question. Do not modify files. Trace relevant code, tests, documentation, and history when useful. Return concise findings with concrete paths, symbols, and any uncertainty. Prefer evidence over speculation.",
135
+ builtin: true,
136
+ workspace: "shared",
137
+ },
138
+ [CODER_AGENT_NAME]: {
139
+ name: CODER_AGENT_NAME,
140
+ description: "Implement and verify changes in the shared source tree.",
141
+ tools: DEFAULT_TOOLS,
142
+ systemPrompt:
143
+ "Implement the assigned change. Read the existing code and project instructions first. Prefer the smallest maintainable solution that follows existing conventions. Surface failures clearly. Run focused tests or checks and report what changed, what passed, and any remaining risk.",
144
+ builtin: true,
145
+ workspace: "shared",
146
+ },
147
+ [REVIEWER_AGENT_NAME]: {
148
+ name: REVIEWER_AGENT_NAME,
149
+ description: "Inspect the current snapshot and report actionable findings.",
150
+ tools: ["read", "bash"],
151
+ systemPrompt:
152
+ "Review the current snapshot for correctness, regressions, security problems, and missing tests. Do not modify the source project. Run focused checks when useful. Report actionable findings ordered by severity, with concrete paths and locations. If there are no material findings, say so plainly; do not invent issues or merely summarize the implementation.",
153
+ builtin: true,
154
+ workspace: "scratch",
155
+ },
156
+ };
157
+
158
+ export function isBuiltinAgentName(name: string): boolean {
159
+ return (BUILTIN_AGENT_NAMES as readonly string[]).includes(name);
160
+ }
161
+
113
162
  /** Find the nearest ancestor containing project-scoped agent files. */
114
163
  export function findProjectRoot(cwd: string): string | null {
115
164
  let dir = cwd;
@@ -330,7 +379,9 @@ export function discoverAgents(cwd: string): Map<string, AgentConfig> {
330
379
  scope: "claude",
331
380
  });
332
381
 
333
- const agents = new Map<string, AgentConfig>();
382
+ const agents = new Map<string, AgentConfig>(
383
+ Object.entries(BUILTIN_AGENT_CONFIGS),
384
+ );
334
385
  const loadDir = (
335
386
  { dir, scope }: { dir: string; scope: AgentConfig["scope"] },
336
387
  loader: (fp: string) => AgentConfig | null,
@@ -345,9 +396,9 @@ export function discoverAgents(cwd: string): Map<string, AgentConfig> {
345
396
  if (!e.name.endsWith(".md") || e.name.endsWith(".chain.md")) continue;
346
397
  const filePath = path.join(dir, e.name);
347
398
  const cfg = loader(filePath);
348
- if (cfg?.name === DEFAULT_AGENT_NAME) {
399
+ if (cfg && isBuiltinAgentName(cfg.name)) {
349
400
  console.warn(
350
- `[delegate] ignoring agent profile '${DEFAULT_AGENT_NAME}' from ${filePath}: the name is reserved for the built-in parent-mirroring profile.`,
401
+ `[delegate] ignoring agent profile '${cfg.name}' from ${filePath}: the name is reserved for a built-in delegate profile.`,
351
402
  );
352
403
  continue;
353
404
  }
package/config.ts CHANGED
@@ -129,10 +129,17 @@ function normalizeProviderExtensions(
129
129
  // Provider-scoped opt-in extension map for subagents. Keep this aligned with
130
130
  // the currently shipped codex remote-compaction integration. `delegate.json`
131
131
  // `providerExtensions` replaces a provider's entries (it does not append); an
132
- // empty array is ignored so the default persists.
133
- const DEFAULT_PROVIDER_EXTENSIONS: Record<string, readonly string[]> = {
134
- "openai-codex": ["git:github.com/bermudi/manaflow-pi-codex"],
135
- };
132
+ // empty array is ignored so the default persists. Provenance is classification
133
+ // by config presence: every source the user lists is required and fails
134
+ // closed when missing, unverifiable, or broken — including an exact re-listing
135
+ // of a shipped default. Shipped defaults (providers the user never mentioned)
136
+ // are best-effort: they degrade silently to extension-free subagents on Pi's
137
+ // native compaction, because absence of an optional integration is not a
138
+ // warning condition — that is Pi's normal operation.
139
+ const DEFAULT_PROVIDER_EXTENSIONS: Record<string, readonly string[]> =
140
+ Object.assign(Object.create(null) as Record<string, readonly string[]>, {
141
+ "openai-codex": ["npm:@bermudi/pi-codex"],
142
+ });
136
143
 
137
144
  function resolveProviderExtensions(
138
145
  raw: unknown,
@@ -155,7 +162,7 @@ const DEFAULT_DELEGATE_CONFIG: DelegateConfig = {
155
162
  wholeTaskMaxRetries: 3,
156
163
  wholeTaskBaseDelayMs: 1_000,
157
164
  },
158
- providerExtensions: DEFAULT_PROVIDER_EXTENSIONS,
165
+ providerExtensions: {},
159
166
  telemetry: {
160
167
  enabled: true,
161
168
  },
@@ -174,7 +181,15 @@ let __delegateConfig: DelegateConfig = {
174
181
  };
175
182
  let stallTimeoutOverrideForTesting: number | undefined;
176
183
 
177
- /** Read delegate config from disk. Returns defaults if file missing or corrupt. */
184
+ /** Read delegate config from disk. Returns defaults if file missing or corrupt.
185
+ *
186
+ * The returned `providerExtensions` is the *user-only* view — exactly what the
187
+ * file said, defaults excluded. `getSubagentProviderExtensionMap()` is the
188
+ * merged (defaults + user) view, and
189
+ * `getSubagentProviderExtensionSourcesForProvider()` is the provenance-tagged
190
+ * view. Keeping the raw user map here is what lets the sources getter
191
+ * distinguish "the user listed this" from "this is a shipped default" by
192
+ * config presence rather than string identity. */
178
193
  export function loadDelegateConfig(): DelegateConfig {
179
194
  try {
180
195
  const raw = fs.readFileSync(DELEGATE_CONFIG_PATH, "utf-8");
@@ -191,7 +206,9 @@ export function loadDelegateConfig(): DelegateConfig {
191
206
  ...(parsed.concurrency ?? {}),
192
207
  },
193
208
  retry: { ...DEFAULT_DELEGATE_CONFIG.retry, ...(parsed.retry ?? {}) },
194
- providerExtensions: resolveProviderExtensions(parsed.providerExtensions),
209
+ providerExtensions: normalizeProviderExtensions(
210
+ parsed.providerExtensions,
211
+ ),
195
212
  telemetry: normalizeTelemetryConfig(parsed.telemetry),
196
213
  output: { ...DEFAULT_DELEGATE_CONFIG.output, ...(parsed.output ?? {}) },
197
214
  } as DelegateConfig;
@@ -255,7 +272,7 @@ export function _setDelegateConfigForTesting(
255
272
  ...DEFAULT_DELEGATE_CONFIG.retry,
256
273
  ...(config.retry ?? {}),
257
274
  },
258
- providerExtensions: resolveProviderExtensions(config.providerExtensions),
275
+ providerExtensions: normalizeProviderExtensions(config.providerExtensions),
259
276
  telemetry: normalizeTelemetryConfig(config.telemetry),
260
277
  output: {
261
278
  ...DEFAULT_DELEGATE_CONFIG.output,
@@ -266,7 +283,11 @@ export function _setDelegateConfigForTesting(
266
283
  }
267
284
 
268
285
  /**
269
- * Get the configured provider-scoped extension allowlist for subagents.
286
+ * Get the configured provider-scoped extension allowlist for subagents:
287
+ * the merged view (shipped defaults + user config, user entries replacing a
288
+ * provider's defaults). The stored `config.providerExtensions` itself is the
289
+ * user-only view; the merge happens here so provenance survives until a
290
+ * consumer asks for it (`getSubagentProviderExtensionSourcesForProvider`).
270
291
  * Explicit configs are normalized here too, so callers using the injected
271
292
  * config form get the same case-insensitive and replace-per-provider
272
293
  * semantics as the file-backed singleton.
@@ -295,6 +316,60 @@ export function getSubagentProviderExtensionsForProvider(
295
316
  : [];
296
317
  }
297
318
 
319
+ /** A provider-extension source together with how it entered the config. */
320
+ export interface ProviderExtensionSource {
321
+ /** The normalized source string, as the package manager consumes it. */
322
+ readonly source: string;
323
+ /**
324
+ * Whether the user configured this source themselves (required) or it is a
325
+ * shipped default for a provider the user never mentioned (best-effort).
326
+ * Required sources fail closed when missing, unverifiable, or broken;
327
+ * best-effort defaults degrade silently to extension-free subagents on
328
+ * Pi's native compaction.
329
+ */
330
+ readonly required: boolean;
331
+ }
332
+
333
+ /**
334
+ * Get the provenance-tagged extension sources for a provider's subagents.
335
+ * Classification is by config presence, never by string identity: everything
336
+ * the user lists in `providerExtensions` is `required: true` — including an
337
+ * exact re-listing of a shipped default, because typing it into the config
338
+ * expresses intent. Providers the user never configured fall back to the
339
+ * shipped defaults, tagged `required: false` (best-effort).
340
+ */
341
+ export function getSubagentProviderExtensionSourcesForProvider(
342
+ provider: string | undefined,
343
+ config: DelegateConfig = __delegateConfig,
344
+ ): readonly ProviderExtensionSource[] {
345
+ const normalized = provider?.trim().toLowerCase();
346
+ if (!normalized) return [];
347
+ // Normalize the injected config map so an unnormalized key like " Custom-Provider "
348
+ // is handled, matching getSubagentProviderExtensionMap / getSubagentProviderExtensionsForProvider.
349
+ const rawUserMap = config.providerExtensions;
350
+ const userMap = rawUserMap
351
+ ? normalizeProviderExtensions(rawUserMap)
352
+ : (Object.create(null) as Record<string, readonly string[]>);
353
+ if (Object.prototype.hasOwnProperty.call(userMap, normalized)) {
354
+ return (userMap[normalized] ?? []).map((source) => ({
355
+ source,
356
+ required: true,
357
+ }));
358
+ }
359
+ if (
360
+ !Object.prototype.hasOwnProperty.call(
361
+ DEFAULT_PROVIDER_EXTENSIONS,
362
+ normalized,
363
+ )
364
+ ) {
365
+ return [];
366
+ }
367
+ return (DEFAULT_PROVIDER_EXTENSIONS[normalized] ?? []).map((source) => ({
368
+ source,
369
+ required: false,
370
+ }));
371
+ }
372
+
298
373
  // ── Config Getters ───────────────────────────────────────────────────────
299
374
 
300
375
  /**
package/constants.ts CHANGED
@@ -1,6 +1,23 @@
1
1
  /** Reserved built-in profile that mirrors the live parent configuration. */
2
2
  export const DEFAULT_AGENT_NAME = "default";
3
3
 
4
+ /** Reserved built-in profile for read-only investigation. */
5
+ export const SCOUT_AGENT_NAME = "scout";
6
+
7
+ /** Reserved built-in profile for shared-workspace implementation work. */
8
+ export const CODER_AGENT_NAME = "coder";
9
+
10
+ /** Reserved built-in profile for isolated review work. */
11
+ export const REVIEWER_AGENT_NAME = "reviewer";
12
+
13
+ /** All names reserved by delegate's built-in profiles. */
14
+ export const BUILTIN_AGENT_NAMES = [
15
+ DEFAULT_AGENT_NAME,
16
+ SCOUT_AGENT_NAME,
17
+ CODER_AGENT_NAME,
18
+ REVIEWER_AGENT_NAME,
19
+ ] as const;
20
+
4
21
  /** Full-capability agent set. Inline-task default and the `*` shorthand.
5
22
  * Bash subsumes search, so the dedicated grep/find/ls tools are excluded. */
6
23
  export const DEFAULT_TOOLS = ["read", "write", "edit", "bash"];
package/delegate.ts CHANGED
@@ -24,6 +24,10 @@ export type { DelegateConfig } from "./config.ts";
24
24
 
25
25
  export {
26
26
  DEFAULT_AGENT_NAME,
27
+ SCOUT_AGENT_NAME,
28
+ CODER_AGENT_NAME,
29
+ REVIEWER_AGENT_NAME,
30
+ BUILTIN_AGENT_NAMES,
27
31
  DEFAULT_TOOLS,
28
32
  READONLY_TOOLS,
29
33
  MAX_CONCURRENCY,
@@ -123,6 +127,8 @@ export {
123
127
  loadAgentFile,
124
128
  loadClaudeAgentFile,
125
129
  discoverAgents,
130
+ BUILTIN_AGENT_CONFIGS,
131
+ isBuiltinAgentName,
126
132
  buildSubagentSystemPrompt,
127
133
  DEFAULT_SUBAGENT_SYSTEM_PROMPT,
128
134
  } from "./agents.ts";
@@ -133,7 +139,12 @@ export {
133
139
  resolveModelRequest,
134
140
  findAvailableAlternative,
135
141
  } from "./model.ts";
136
- export { readDelegateSettingsFile, loadDelegateSettings } from "./settings.ts";
142
+ export {
143
+ readDelegateSettingsFile,
144
+ loadDelegateSettings,
145
+ clearDelegateSettingsCache,
146
+ } from "./settings.ts";
147
+ export type { AgentOverride } from "./settings.ts";
137
148
  export { resolveCwd, extractOutput, extractUsage } from "./utils.ts";
138
149
  export {
139
150
  decideSpill,
package/dispatch.ts CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  import { validateDelegateOperation } from "./schema.ts";
25
25
  import { notifyCrossLeafDelivery, syncDelegateStatus } from "./status.ts";
26
26
  import { validateTasks, resolveTasks } from "./task-resolution.ts";
27
+ import { clearDelegateSettingsCache } from "./settings.ts";
27
28
  import type { CallSpan } from "./telemetry.ts";
28
29
  import type {
29
30
  AgentConfig,
@@ -146,6 +147,9 @@ export interface DelegateDispatchInput {
146
147
  export async function dispatchDelegate(
147
148
  input: DelegateDispatchInput,
148
149
  ): Promise<DelegateToolResult> {
150
+ // Settings are user-editable. Clear once at the dispatch boundary so every
151
+ // task in this batch observes one consistent settings snapshot.
152
+ clearDelegateSettingsCache();
149
153
  const {
150
154
  pi,
151
155
  params,
package/extension.ts CHANGED
@@ -18,7 +18,10 @@ import {
18
18
  } from "./dispatch.ts";
19
19
  import { renderDelegateCall, renderDelegateResult } from "./render-result.ts";
20
20
  import { hostCompatError } from "./host-compat.ts";
21
- import { invalidateHostDepsCache } from "./host.ts";
21
+ import {
22
+ invalidateHostDepsCache,
23
+ registerProviderExtensionNotifier,
24
+ } from "./host.ts";
22
25
  import { recordTreeNavigation, resetLeafTracking } from "./leaf.ts";
23
26
  import { closeAllPooledAgents } from "./pool.ts";
24
27
  import {
@@ -122,6 +125,12 @@ export default function delegateExtension(pi: ExtensionAPI): void {
122
125
  prepareArguments: normalizeDelegateArguments,
123
126
 
124
127
  async execute(_id, params: DelegateArguments, signal, onUpdate, ctx) {
128
+ // Prime the UI notice for best-effort provider extensions that load for
129
+ // subagents (host.ts consumes it where the fact is discovered). Every
130
+ // execute re-primes so a stale ctx never sticks.
131
+ registerProviderExtensionNotifier((message) =>
132
+ ctx.ui.notify(message, "info"),
133
+ );
125
134
  const parentModelId = ctx.model?.id;
126
135
  const tasks = params.tasks ?? [];
127
136
  const parentSessionFile = (
@@ -347,6 +356,7 @@ export default function delegateExtension(pi: ExtensionAPI): void {
347
356
  // captured pi) to touch. The cancelled completion path still writes one
348
357
  // final aggregate after late task results arrive, but never delivers UI.
349
358
  clearDelegateStatusContext();
359
+ registerProviderExtensionNotifier(undefined);
350
360
  // A replacement session starts on its own leaf; stale tracking would make
351
361
  // every ticket look cross-leaf (or, worse, look same-leaf by accident).
352
362
  resetLeafTracking();