@narumitw/pi-subagents 0.53.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +85 -15
  2. package/package.json +1 -1
  3. package/src/agents/built-ins.ts +124 -0
  4. package/src/agents/catalog.ts +224 -0
  5. package/src/agents/discovery.ts +249 -0
  6. package/src/agents/types.ts +98 -0
  7. package/src/agents.ts +47 -670
  8. package/src/auto-transport.ts +2 -1
  9. package/src/automation.ts +7 -2
  10. package/src/capability-router.ts +1 -1
  11. package/src/completion-delivery.ts +82 -11
  12. package/src/config-status.ts +2 -2
  13. package/src/config-ui.ts +8 -8
  14. package/src/consult-resources.ts +1 -1
  15. package/src/consult.ts +9 -7
  16. package/src/create-stateful-transport.ts +2 -1
  17. package/src/cwd-policy.ts +1 -1
  18. package/src/execution/budget.ts +56 -0
  19. package/src/execution/runtime-policy.ts +19 -0
  20. package/src/execution-plan.ts +1 -1
  21. package/src/execution-profiles.ts +1 -1
  22. package/src/execution-ui.ts +1 -1
  23. package/src/execution.ts +269 -100
  24. package/src/in-process-transport.ts +3 -2
  25. package/src/inspect.ts +39 -8
  26. package/src/limits.ts +1 -0
  27. package/src/orchestration-metrics.ts +12 -5
  28. package/src/panel-execution.ts +1 -1
  29. package/src/panel-planning.ts +1 -1
  30. package/src/params.ts +3 -1
  31. package/src/persistence.ts +106 -7
  32. package/src/registry-types.ts +22 -5
  33. package/src/registry.ts +205 -37
  34. package/src/render.ts +1 -1
  35. package/src/retained-semantic-state.ts +1 -1
  36. package/src/rpc-transport-metadata.ts +1 -1
  37. package/src/rpc-transport.ts +2 -1
  38. package/src/runner.ts +6 -1
  39. package/src/settings/inspection.ts +275 -0
  40. package/src/settings/schema.ts +186 -0
  41. package/src/settings.ts +72 -420
  42. package/src/spawn-idempotency.ts +1 -1
  43. package/src/stateful-agent-view.ts +87 -0
  44. package/src/stateful-config.ts +1 -1
  45. package/src/stateful-guidance.ts +2 -2
  46. package/src/stateful-limits.ts +1 -1
  47. package/src/stateful-prompt.ts +2 -2
  48. package/src/stateful-render.ts +0 -1
  49. package/src/stateful-safety.ts +2 -1
  50. package/src/stateful-tool-params.ts +13 -20
  51. package/src/stateful.ts +36 -117
  52. package/src/subagents.ts +8 -9
  53. package/src/subprocess-transport.ts +2 -6
  54. package/src/transport-types.ts +1 -1
  55. package/src/transport-ui.ts +1 -1
  56. package/src/verification-harness.ts +516 -0
  57. package/src/verification-receipt.ts +275 -0
  58. package/src/verified-execution-benchmark.ts +86 -0
  59. package/src/verified-execution-contract.ts +219 -0
  60. package/src/work-item-ledger.ts +510 -37
  61. package/src/work-item-persistence.ts +31 -0
  62. package/src/workflow-completion-controller.ts +397 -0
  63. package/src/workflow-plan-compiler.ts +1 -1
  64. package/src/workflow-plan-patch.ts +1 -1
  65. package/src/workflow-planning.ts +11 -1
  66. package/src/workflow-ui.ts +1 -1
package/README.md CHANGED
@@ -299,8 +299,8 @@ No benchmark result in this release changes the default delegation policy or mak
299
299
  | --- | --- | --- |
300
300
  | `list_agents` | Optional `agentScope` (default `user`) and `limit` (default 32, maximum 100) | Bounded agent metadata and omission counts |
301
301
  | `get_agent` | Required `agent`; optional `agentScope` | One resolved definition, safe source path, configured tools, and consultation-effective tools; never the system prompt |
302
- | `list_runs` | Optional `includeClosed` and `limit` (default 50, maximum 100) | Metadata-only retained-run summaries and unread counts |
303
- | `get_run` | Required `agentId` | Safe `cwd`, current-task/error summaries, thinking level, context footprint, protocol, effective transport, bounded timing/usage telemetry, structured result when valid, policy, history count, and unread count |
302
+ | `list_runs` | Optional `includeClosed` and `limit` (default 50, maximum 100) | Metadata-only retained-run summaries, turn generation, pending-completion count, and unread counts |
303
+ | `get_run` | Required `agentId` | Safe `cwd`, current run and turn generation, current-task/error summaries, thinking level, context footprint, protocol, effective transport, bounded timing/usage telemetry, structured result when valid, policy, history count, pending-completion count, and unread count |
304
304
  | `list_workflows` | Optional `limit` (default 50, maximum 100) | Metadata-only persisted blocking-workflow summaries for the current session |
305
305
  | `get_workflow` | Required `workflowId` | Bounded task states, generations, dependencies, plan identities, artifact metadata, verification state, and outcome reasons without artifact contents |
306
306
  | `list_models` | Optional `limit` (default 50, maximum 100) | Session-scoped models, or the already-loaded available snapshot |
@@ -310,7 +310,7 @@ No benchmark result in this release changes the default delegation policy or mak
310
310
 
311
311
  The schema rejects fields that do not belong to the selected action. Explicit `project` or `both` scope fails before project-agent discovery unless Pi already trusts the project. Run inspection never returns history output, stored context, or mailbox content; unread counts come from a metadata-only snapshot and do not acknowledge messages. Workflow inspection reads validated, redacted snapshots without quarantining or rewriting invalid files. Paths beneath the Pi agent directory use `~`, project paths are workspace-relative, model objects are projected through an allow-list, and model-facing text is bounded to 50 KiB or 2,000 lines.
312
312
 
313
- Compatibility: `subagent_manage({ "action": "list" })` remains supported with its existing behavior. Prefer `subagent_inspect` when a whole tool must be safe to activate on a read-only surface.
313
+ `subagent_manage` no longer accepts its former compatibility `list` action. Use `subagent_inspect({ "action": "list_runs", "includeClosed": true })` for metadata-only discovery and `get_run` for detail.
314
314
 
315
315
  ## 📖 Read-only consultation
316
316
 
@@ -480,7 +480,55 @@ Run an explicit dependency workflow:
480
480
  }
481
481
  ```
482
482
 
483
- A verification-gated implementation declares one distinct verifier:
483
+ Managed verified execution is an explicit per-workflow contract.
484
+ The executor infers the final mutating integration owner when none is declared, synthesizes one distinct read-only verifier, runs declared deterministic checks in a disposable Git worktree overlaid with the submitted state, and accepts only the exact unchanged submitted state.
485
+ Every deterministic check has a stable evidence ID, a direct executable with argument-array invocation, and an optional relative `cwd` and timeout.
486
+ Only `git`, `node`, `npm`, and `npx` are accepted; shell command strings fail before child allocation.
487
+ The integration owner must request `structured-v2`, declare a non-empty `writePaths` scope, and name current required evidence through its delegation contract.
488
+ Every required evidence ID must match a currently passed executor-owned check; worker-authored artifact metadata never satisfies that binding.
489
+
490
+ ```json
491
+ {
492
+ "workflow": {
493
+ "verifiedExecution": {
494
+ "verifierAgent": "reviewer",
495
+ "maxReworkCycles": 1,
496
+ "checks": [
497
+ {
498
+ "id": "focused-test",
499
+ "command": "npm",
500
+ "args": ["test", "--", "feature"],
501
+ "timeoutMs": 120000
502
+ }
503
+ ]
504
+ },
505
+ "tasks": [
506
+ {
507
+ "id": "implementation",
508
+ "agent": "worker",
509
+ "task": "Implement the contracted change.",
510
+ "writePaths": ["src", "test"],
511
+ "acceptanceCriteria": ["The focused regression test passes"],
512
+ "resultFormat": "structured-v2",
513
+ "contract": {
514
+ "version": "pi-subagents:delegation:v2",
515
+ "level": "full",
516
+ "taskId": "implementation",
517
+ "objective": "Implement the contracted change",
518
+ "requiredEvidence": ["focused-test"],
519
+ "sideEffectPolicy": "mutating"
520
+ }
521
+ }
522
+ ]
523
+ }
524
+ }
525
+ ```
526
+
527
+ An advanced caller may provide the verifier task instead of letting the executor synthesize it.
528
+ That task must directly and only depend on the integration owner, use `structured-v2`, select the configured distinct verifier agent, and declare an enforced read-only contract without shell or custom tools.
529
+ The executor narrows accepted verifier authority to `read` even when the selected agent normally has broader tools, and disables verifier extensions, skills, prompt templates, and inherited context files.
530
+
531
+ The older explicit verifier contract remains available as a compatibility gate without managed integration:
484
532
 
485
533
  ```json
486
534
  {
@@ -528,16 +576,32 @@ A task that explicitly requires independent verification must have exactly one d
528
576
  The producer stops in `awaiting-verification`, its own passing verification claims remain untrusted, and ordinary downstream tasks stay blocked until the executor records an accepted verifier receipt.
529
577
  The verifier runs alone in a fresh subprocess context against one bounded Git-visible tree identity and must encode `verification-accepted`, `verification-rework`, or `verification-rejected` through the documented `structured-v2` status and reason fields.
530
578
  Dirty-tree identity covers at most 1 MiB across separately framed staged and unstaged binary diffs plus bounded non-ignored untracked paths and bytes; submodules, unsupported states, and changing trees fail closed.
531
- A rework or rejection preserves bounded evidence but does not replay the producer automatically.
532
- This acceptance gate does not isolate operating-system effects and does not make shared-workspace mutation into manager-controlled patch integration.
533
- Explicit workflow transitions are also atomically persisted as mode-0600, private-text-redacted snapshots for current-session `list_workflows` and `get_workflow` inspection; running and awaiting-verification tasks inspect as `interrupted`, and no prior side effect is automatically resumed.
534
- When a v1 ledger is restored, legacy self-reported verification flags and artifact trust are cleared because they have no executor receipt.
579
+ The compatibility gate preserves bounded rework or rejection evidence but does not replay the producer automatically.
580
+
581
+ With `verifiedExecution`, execution completion and acceptance are separate `pi-subagents:work-acceptance:v1` states.
582
+ A worker's own verification, confidence, prose, consensus, or exit status cannot move `pending` acceptance to `accepted`.
583
+ The executor-owned `pi-subagents:verification-receipt:v1` binds both tree captures, patch digest, changed paths, accepted scope, target and verifier generations and `ExecutionPlan` IDs, verifier identity, acceptance criteria, required current evidence, and bounded deterministic check receipts.
584
+ Each receipt is capped at 12 KiB, each stored check stream at 2 KiB, and oversized acceptance evidence fails closed rather than expanding tool details.
585
+ The verifier receives the original objective, current artifact metadata, immutable tree identity, and executor-owned check output rather than the worker narrative.
586
+ Verifier mutation, a stale or replaced generation, a failed or unsafe check, missing evidence, wrong scope, patch, plan, tree, or identity, cancellation, timeout, and unsupported Git state all produce non-success.
587
+ One verifier `rework` decision may rotate the worker and verifier generations when `maxReworkCycles` is `1`; prior grants are revoked, prior evidence remains history, only current requirements and findings are added, and a second rejection is terminal.
588
+ Crashes, timeouts, cancellation, ambiguous settlement, and drift are never replayed.
589
+ The disposable check worktree includes bounded tracked and non-ignored untracked files and is removed after checks.
590
+ When the repository has a local `node_modules` directory, the worktree is nested beneath it so normal Node and npm resolution can read the installed dependency tree without copying it.
591
+ The worktree isolates repository build output, but it does not make the installed dependency tree read-only and is not an operating-system sandbox for processes, network, secrets, absolute paths, or host credentials.
592
+ The accepted state remains the selected shared workspace; no general patch merge or conflict resolver is added.
593
+
594
+ Omitting `verifiedExecution` preserves prior workflow behavior, including the older explicit verifier gate above.
595
+ To downgrade, finish active workflows, remove `verifiedExecution`, and either use the explicit `verifierFor` compatibility form or perform verification in the parent.
596
+ Older package versions reject the unknown managed contract rather than silently providing its guarantees.
597
+ Explicit workflow transitions are atomically persisted as mode-0600, private-text-redacted snapshots for current-session `list_workflows` and `get_workflow` inspection; in-flight execution or acceptance restores as interrupted non-success, and no prior side effect is automatically resumed.
598
+ Legacy v1 and v2 records without acceptance fields retain their prior completed terminal meaning, while v1 self-reported verification flags and artifact trust remain untrusted.
535
599
 
536
600
  ## 🔁 Stateful agents
537
601
 
538
- Stateful lifecycle tools are available by default. `subagent_spawn` is detached: it schedules work, returns immediately with an opaque `agentId`, and later injects a bounded `pi-subagent-completion` custom message. Completions that settle in the same dispatch window are batched, and the broker allows at most one in-flight root wake until that parent turn starts.
602
+ Stateful lifecycle tools are available by default. `subagent_spawn` is detached: it schedules work, returns immediately with an opaque `agentId`, and later injects a bounded `pi-subagent-completion` custom message. Every turn receives an executor-owned `runId`, monotonically increasing agent-local generation, and unique `completionId`. The terminal completion is persisted before delivery, simultaneous completions are batched, and the broker allows at most one in-flight root wake until that parent turn starts.
539
603
 
540
- Detached work follows a non-polling policy. With default `next-turn` delivery, prefer one bounded `subagent_spawn` for related asynchronous research or review only when the current response does not depend on its result. If it does, use blocking `subagent` when registered; in **Async only**, complete required work directly, opt into `auto-resume` when a later synthesis turn is appropriate, or switch workflows. With opt-in `auto-resume`, detached broad work may be final-answer-dependent because completion requests a synthesis turn after the root settles. In either mode, do useful non-overlapping main-agent work immediately, do not poll `subagent_manage` with `action: "list"` or `subagent_mailbox` with `action: "read"`, and do not duplicate delegated work. Add another detached agent only for truly independent work with safe workspace concurrency. Detached lifecycle work intentionally has no `subagent_wait` tool.
604
+ Detached work follows a non-polling policy. With default `next-turn` delivery, prefer one bounded `subagent_spawn` for related asynchronous research or review only when the current response does not depend on its result. If it does, use blocking `subagent` when registered; in **Async only**, complete required work directly, opt into `auto-resume` when a later synthesis turn is appropriate, or switch workflows. With opt-in `auto-resume`, detached broad work may be final-answer-dependent because completion requests a synthesis turn after the root settles. In either mode, do useful non-overlapping main-agent work immediately, do not poll `subagent_inspect` or `subagent_mailbox` with `action: "read"`, and do not duplicate delegated work. Add another detached agent only for truly independent work with safe workspace concurrency. Detached lifecycle work intentionally has no `subagent_wait` tool.
541
605
 
542
606
  A detached agent additionally needs a concrete isolation or specialization benefit such as independent review, bounded context/output, a distinct model/tool profile, or workspace isolation. Simple work that the main agent can perform directly should not be delegated.
543
607
 
@@ -546,7 +610,7 @@ A detached agent additionally needs a concrete isolation or specialization benef
546
610
  - `"next-turn"` (default) preserves the previous behavior: use `deliverAs: "steer"` with `triggerTurn: false`. An active root can consume completion naturally; an idle root is not awakened.
547
611
  - `"auto-resume"` holds completion while the root is active, then requests one synthesis turn after the parent settles when no user or extension messages are already pending. Simultaneous completions share that turn, active work is not interrupted, and pending input suppresses the autonomous wake.
548
612
 
549
- Auto-resume is best-effort because Pi's custom-message API is fire-and-forget. Session-generation checks, shutdown cleanup, batching, and the in-flight wake guard prevent stale or duplicate scheduling pressure, but they do not make completion delivery durable across process exit.
613
+ The bounded persisted completion outbox provides ordered at-least-once delivery across process restart without replaying the child turn. When state must be reduced to its storage bound, persistence drops roots without pending completions first and trims old history rather than discarding an outbox-owned root. A completion is acknowledged only after parent context assembly observes its exact `completionId`; an injection that returns synchronously but never reaches context remains pending for retry. If the process exits after context assembly but before acknowledgement is persisted, the same ID can be delivered again and consumers must deduplicate it. Auto-resume wake admission remains best-effort because Pi's custom-message API is fire-and-forget, but an unacknowledged terminal completion itself remains available for redelivery on the next start of the owning session. Transient terminal-persistence failures retry with bounded exponential backoff and keep the run pending; shutdown cancels retry waits and reports a final persistence failure instead of silently resolving unsaved work.
550
614
 
551
615
  The default `subprocess` transport preserves compatibility: each turn starts a fresh isolated `pi --mode json -p --no-session` child and receives sanitized, bounded history.
552
616
  Set `transport` to `in-process` to retain one public Pi SDK `AgentSession` per stateful `agentId`, avoiding repeated process startup while preserving native child history in memory.
@@ -623,7 +687,7 @@ This avoids lifecycle-driven tool-schema churn and preserves a stable provider p
623
687
  | --- | --- |
624
688
  | `subagent_spawn` | Start detached work with optional task-selected thinking and retained timeout, exact-retry `idempotencyKey`, and `text`, `structured-v1`, or `structured-v2` result format; return an opaque `agentId` immediately and deliver completion asynchronously. |
625
689
  | `subagent_send` | Send follow-up work with an optional one-turn timeout override and trigger a new turn on a reusable agent; semantic skew requires explicit `revalidate: true`, and shared-workspace write conflicts are guarded unless explicitly overridden. |
626
- | `subagent_manage` | Use `action: "list"` to inspect agents, `"interrupt"` to retain an agent after aborting active work, or `"close"` to release it; interrupt/close accept optional `subtree`. |
690
+ | `subagent_manage` | Use `"interrupt"` to retain an agent after aborting active work or `"close"` to release it; both actions accept optional `subtree`. Use `subagent_inspect` for all list and detail operations. |
627
691
  | `subagent_mailbox` | Use `action: "send"` for queue-only messages that do not start a turn, or `"read"` to read and optionally acknowledge unread messages. |
628
692
 
629
693
  The action schemas are flat for provider compatibility and reject parameters that belong to another action. For example:
@@ -657,13 +721,14 @@ The five replaced names are intentionally not registered as aliases. Update expl
657
721
 
658
722
  | Previous call | Fixed-surface call |
659
723
  | --- | --- |
660
- | `subagent_list({ includeClosed })` | `subagent_manage({ action: "list", includeClosed })` |
724
+ | `subagent_list({ includeClosed })` | `subagent_inspect({ action: "list_runs", includeClosed })` |
725
+ | `subagent_manage({ action: "list", includeClosed })` | `subagent_inspect({ action: "list_runs", includeClosed })` |
661
726
  | `subagent_interrupt({ agentId, subtree })` | `subagent_manage({ action: "interrupt", agentId, subtree })` |
662
727
  | `subagent_close({ agentId, subtree })` | `subagent_manage({ action: "close", agentId, subtree })` |
663
728
  | `subagent_message({ agentId, message, ... })` | `subagent_mailbox({ action: "send", agentId, message, ... })` |
664
729
  | `subagent_messages({ agentId, acknowledge, limit })` | `subagent_mailbox({ action: "read", agentId, acknowledge, limit })` |
665
730
 
666
- Persisted agent and mailbox records require no migration. If an explicit prompt in a resumed conversation keeps requesting an old name, update it with the mapping above or start a fresh conversation. To roll back after an upgrade, pin the package version used before the upgrade; for this migration, use `pi install npm:@narumitw/pi-subagents@0.26.0`. The previous release can read the same state directory.
731
+ Persisted agent and mailbox records require no manual migration; older records load with an empty completion outbox and generation zero. If an explicit prompt in a resumed conversation keeps requesting an old name, update it with the mapping above or start a fresh conversation. To roll back after an upgrade, pin the package version used before the upgrade; for this migration, use `pi install npm:@narumitw/pi-subagents@0.26.0`. The previous release can read the same state directory.
667
732
 
668
733
  A spawn can request a thinking level explicitly:
669
734
 
@@ -1010,7 +1075,12 @@ packages/pi-subagents/
1010
1075
  │ ├── execution-plan.ts # Executor-owned authority and resource resolution
1011
1076
  │ ├── work-item-ledger.ts # Persistent dependency and artifact state machine
1012
1077
  │ ├── work-item-persistence.ts # Atomic redacted workflow state and inspection
1013
- │ ├── workflow-verification.ts # Executor-owned independent-verifier receipts
1078
+ │ ├── workflow-verification.ts # Compatibility independent-verifier receipts
1079
+ │ ├── verified-execution-contract.ts # Explicit managed-verification request boundary
1080
+ │ ├── workflow-completion-controller.ts # Sole opted-in terminal acceptance owner
1081
+ │ ├── verification-harness.ts # Disposable deterministic check execution
1082
+ │ ├── verification-receipt.ts # Strict executor-owned managed receipts
1083
+ │ ├── verified-execution-benchmark.ts # Matched offline acceptance/cost fixture
1014
1084
  │ ├── workflow-tree-identity.ts # Bounded exact Git-visible tree identities
1015
1085
  │ ├── integration-controller.ts # Fail-closed canonical integration admission
1016
1086
  │ ├── adaptive-scheduler.ts # Dependency, capacity, budget, and conflict scheduling
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-subagents",
3
- "version": "0.53.0",
3
+ "version": "1.0.0",
4
4
  "description": "Pi extension for delegating work to specialized isolated subagents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Built-in agent definitions and prompt construction.
3
+ */
4
+
5
+ import { type AgentCapabilityManifest, CAPABILITY_MANIFEST_VERSION } from "../capabilities.js";
6
+ import type { AgentConfig } from "./types.js";
7
+
8
+ export const BUILT_IN_AGENTS: AgentConfig[] = [
9
+ {
10
+ name: "scout",
11
+ description:
12
+ "Read-only codebase reconnaissance; returns concise findings with paths and evidence.",
13
+ tools: ["read", "grep", "find", "ls", "bash"],
14
+ capabilityManifest: builtInManifest(["repository-search", "code-evidence"], "read", [
15
+ "evidence-gathering",
16
+ ]),
17
+ source: "built-in",
18
+ filePath: "built-in:scout",
19
+ systemPrompt: [
20
+ "You are a scout subagent. Explore the codebase quickly and report grounded findings.",
21
+ "Do not edit files. Prefer read, grep, find, ls, and safe bash inspection commands.",
22
+ "Return concise bullets with exact file paths, symbols, and open questions.",
23
+ ].join("\n"),
24
+ },
25
+ {
26
+ name: "planner",
27
+ description: "Turns reconnaissance into a lean implementation or migration plan.",
28
+ tools: ["read", "grep", "find", "ls"],
29
+ capabilityManifest: builtInManifest(
30
+ ["task-decomposition", "implementation-planning", "migration-planning"],
31
+ "read",
32
+ ),
33
+ source: "built-in",
34
+ filePath: "built-in:planner",
35
+ systemPrompt: [
36
+ "You are a planner subagent. Produce executable, verifiable plans only.",
37
+ "Do not modify files. Ground the plan in the repository's actual structure.",
38
+ "Call out assumptions, risks, sequencing, and verification commands.",
39
+ ].join("\n"),
40
+ },
41
+ {
42
+ name: "reviewer",
43
+ description: "Independent code review agent that inspects existing verification evidence.",
44
+ tools: ["read", "grep", "find", "ls", "bash"],
45
+ capabilityManifest: builtInManifest(
46
+ ["code-review", "evidence-review", "security-baseline"],
47
+ "read",
48
+ ["independent-review"],
49
+ ),
50
+ source: "built-in",
51
+ filePath: "built-in:reviewer",
52
+ systemPrompt: [
53
+ "You are a reviewer subagent. Review changes adversarially and assess claims against the code and existing evidence.",
54
+ "Do not edit files or run tests, builds, benchmarks, formatters, or other long-running verification commands.",
55
+ "Inspect code, diffs, test definitions, and existing verification evidence. Recommend any additional commands for the main agent to run.",
56
+ "Report PASS, FAIL, or PARTIAL with evidence, commands inspected, and specific follow-ups.",
57
+ ].join("\n"),
58
+ },
59
+ {
60
+ name: "worker",
61
+ description: "General-purpose implementation worker with the default Pi tool set.",
62
+ capabilityManifest: builtInManifest(
63
+ ["implementation", "command-execution", "repository-modification"],
64
+ "write",
65
+ ),
66
+ source: "built-in",
67
+ filePath: "built-in:worker",
68
+ systemPrompt: workerSystemPrompt(),
69
+ },
70
+ {
71
+ name: "general",
72
+ description: "Alias for worker; kept for model-generated subagent names.",
73
+ capabilityManifest: builtInManifest(
74
+ ["implementation", "command-execution", "repository-modification"],
75
+ "write",
76
+ ),
77
+ source: "built-in",
78
+ filePath: "built-in:general",
79
+ systemPrompt: workerSystemPrompt(),
80
+ },
81
+ {
82
+ name: "general-purpose",
83
+ description: "Alias for worker; compatible with common subagent naming conventions.",
84
+ capabilityManifest: builtInManifest(
85
+ ["implementation", "command-execution", "repository-modification"],
86
+ "write",
87
+ ),
88
+ source: "built-in",
89
+ filePath: "built-in:general-purpose",
90
+ systemPrompt: workerSystemPrompt(),
91
+ },
92
+ ];
93
+
94
+ export function getBuiltInAgent(name: string): AgentConfig | undefined {
95
+ const agent = BUILT_IN_AGENTS.find((candidate) => candidate.name === name);
96
+ return agent ? structuredClone(agent) : undefined;
97
+ }
98
+
99
+ function builtInManifest(
100
+ capabilities: string[],
101
+ filesystem: "read" | "write",
102
+ verificationRoles: string[] = [],
103
+ ): AgentCapabilityManifest {
104
+ return {
105
+ version: CAPABILITY_MANIFEST_VERSION,
106
+ capabilities,
107
+ modalities: ["text"],
108
+ resultFormats: ["text", "structured-v1", "structured-v2"],
109
+ authority: { filesystem },
110
+ verificationRoles,
111
+ contextStrengths: ["repository"],
112
+ costHint: filesystem === "read" ? "low" : "medium",
113
+ latencyHint: filesystem === "read" ? "low" : "medium",
114
+ limitations: [],
115
+ };
116
+ }
117
+
118
+ function workerSystemPrompt(): string {
119
+ return [
120
+ "You are a focused worker subagent running in an isolated Pi process.",
121
+ "Complete the delegated task directly. Keep scope tight and avoid unrelated changes.",
122
+ "When done, summarize files changed, commands run, and any remaining risks.",
123
+ ].join("\n");
124
+ }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Bounded, sanitized model-facing agent catalog formatting.
3
+ */
4
+
5
+ import { BUILT_IN_AGENTS } from "./built-ins.js";
6
+ import {
7
+ type AgentDiscoveryOptions,
8
+ type AgentDiscoveryResult,
9
+ discoverAgents,
10
+ } from "./discovery.js";
11
+ import type { AgentConfig, SubagentSettings } from "./types.js";
12
+
13
+ export function formatAgentList(
14
+ agents: AgentConfig[],
15
+ maxItems: number,
16
+ ): { text: string; remaining: number } {
17
+ if (agents.length === 0) return { text: "none", remaining: 0 };
18
+ const listed = agents.slice(0, maxItems);
19
+ const remaining = agents.length - listed.length;
20
+ return {
21
+ text: listed.map((a) => `${a.name} (${a.source}): ${a.description}`).join("; "),
22
+ remaining,
23
+ };
24
+ }
25
+
26
+ export interface AgentCatalog {
27
+ /** The effective catalog for the default invocation scope. */
28
+ user: AgentDiscoveryResult;
29
+ /** The project-scope catalog; custom project definitions are loaded only after project trust. */
30
+ project?: AgentDiscoveryResult;
31
+ }
32
+
33
+ export interface AgentCatalogFormatOptions {
34
+ maxItems?: number;
35
+ maxDescriptionLength?: number;
36
+ maxCharacters?: number;
37
+ }
38
+
39
+ export interface AgentCatalogFormatResult {
40
+ text: string;
41
+ omitted: number;
42
+ }
43
+
44
+ export const DEFAULT_AGENT_CATALOG_MAX_ITEMS = 32;
45
+ export const DEFAULT_AGENT_CATALOG_MAX_DESCRIPTION_LENGTH = 240;
46
+ export const DEFAULT_AGENT_CATALOG_MAX_CHARACTERS = 6_000;
47
+ export const DEFAULT_AGENT_CATALOG_MAX_FILES_PER_SCOPE = 128;
48
+ export const DEFAULT_AGENT_CATALOG_MAX_FILE_BYTES = 64 * 1024;
49
+ export const DEFAULT_AGENT_CATALOG_MAX_TOTAL_BYTES_PER_SCOPE = 2 * 1024 * 1024;
50
+
51
+ const BUILT_IN_AGENT_ORDER = new Map(BUILT_IN_AGENTS.map((agent, index) => [agent.name, index]));
52
+
53
+ function compareCatalogAgents(left: AgentConfig, right: AgentConfig): number {
54
+ const leftBuiltInOrder = BUILT_IN_AGENT_ORDER.get(left.name);
55
+ const rightBuiltInOrder = BUILT_IN_AGENT_ORDER.get(right.name);
56
+ if (leftBuiltInOrder !== undefined || rightBuiltInOrder !== undefined) {
57
+ if (leftBuiltInOrder === undefined) return 1;
58
+ if (rightBuiltInOrder === undefined) return -1;
59
+ return leftBuiltInOrder - rightBuiltInOrder;
60
+ }
61
+ return left.name.localeCompare(right.name);
62
+ }
63
+
64
+ function normalizeCatalogDescription(description: string, maxLength: number): string {
65
+ const normalized = description.replace(/\s+/gu, " ").trim();
66
+ if (normalized.length <= maxLength) return normalized;
67
+ const suffix = "…";
68
+ return `${normalized.slice(0, Math.max(0, maxLength - suffix.length)).trimEnd()}${suffix}`;
69
+ }
70
+
71
+ type CatalogScope = "user" | "project" | "project-fallback";
72
+
73
+ function catalogAgentLine(
74
+ agent: AgentConfig,
75
+ scope: CatalogScope,
76
+ userNames: ReadonlySet<string>,
77
+ maxDescriptionLength: number,
78
+ ): string {
79
+ const scopeLabel =
80
+ scope === "user"
81
+ ? 'agentScope: "user"'
82
+ : scope === "project"
83
+ ? 'requires agentScope: "project" or "both"'
84
+ : 'requires agentScope: "project" ("both" selects the user definition)';
85
+ const collision =
86
+ scope !== "user" && userNames.has(agent.name)
87
+ ? scope === "project"
88
+ ? "; overrides the default user definition for project/both"
89
+ : "; scope-specific fallback for the default user override"
90
+ : "";
91
+ return `- ${agent.name} [source: ${agent.source}; ${scopeLabel}${collision}] — ${normalizeCatalogDescription(agent.description, maxDescriptionLength)}`;
92
+ }
93
+
94
+ /**
95
+ * Format the effective agent variants that the parent model can invoke.
96
+ *
97
+ * User-authored descriptions are prompt text, so this formatter deliberately normalizes and bounds
98
+ * them. Project definitions are supplied separately by the caller so an untrusted project is never
99
+ * read merely to build model-facing metadata.
100
+ */
101
+ export function formatAgentCatalog(
102
+ catalog: AgentCatalog,
103
+ options: AgentCatalogFormatOptions = {},
104
+ ): AgentCatalogFormatResult {
105
+ const maxItems = Math.max(0, options.maxItems ?? DEFAULT_AGENT_CATALOG_MAX_ITEMS);
106
+ const maxDescriptionLength = Math.max(
107
+ 1,
108
+ options.maxDescriptionLength ?? DEFAULT_AGENT_CATALOG_MAX_DESCRIPTION_LENGTH,
109
+ );
110
+ const maxCharacters = Math.max(1, options.maxCharacters ?? DEFAULT_AGENT_CATALOG_MAX_CHARACTERS);
111
+ const userDiscoveryIncomplete =
112
+ (catalog.user.omittedAgentDefinitions ?? 0) > 0 ||
113
+ catalog.user.metadataDiscoveryIncomplete === true;
114
+ const projectDiscoveryIncomplete =
115
+ (catalog.project?.omittedAgentDefinitions ?? 0) > 0 ||
116
+ catalog.project?.metadataDiscoveryIncomplete === true;
117
+ const discoveredUserAgents = [...catalog.user.agents].sort(compareCatalogAgents);
118
+ const discoveredProjectScopeAgents = [...(catalog.project?.agents ?? [])].sort(
119
+ compareCatalogAgents,
120
+ );
121
+ const userAgents = userDiscoveryIncomplete ? [] : discoveredUserAgents;
122
+ const projectScopeAgents = projectDiscoveryIncomplete ? [] : discoveredProjectScopeAgents;
123
+ const projectAgents = projectScopeAgents.filter((agent) => agent.source === "project");
124
+ const discoveredUserByName = new Map(discoveredUserAgents.map((agent) => [agent.name, agent]));
125
+ const userByName = new Map(userAgents.map((agent) => [agent.name, agent]));
126
+ const userNames = new Set(userByName.keys());
127
+ const potentialProjectFallbackAgents = discoveredProjectScopeAgents.filter(
128
+ (agent) =>
129
+ agent.source === "built-in" && discoveredUserByName.get(agent.name)?.source === "user",
130
+ );
131
+ const projectFallbackAgents =
132
+ userDiscoveryIncomplete || projectDiscoveryIncomplete ? [] : potentialProjectFallbackAgents;
133
+ const allEntries = [
134
+ ...userAgents.map((agent) => ({ agent, scope: "user" as const })),
135
+ ...projectAgents.map((agent) => ({ agent, scope: "project" as const })),
136
+ ...projectFallbackAgents.map((agent) => ({ agent, scope: "project-fallback" as const })),
137
+ ];
138
+ const boundedEntries = allEntries.slice(0, maxItems);
139
+ const suppressedMetadataEntries =
140
+ (userDiscoveryIncomplete ? discoveredUserAgents.length : 0) +
141
+ (projectDiscoveryIncomplete
142
+ ? discoveredProjectScopeAgents.filter((agent) => agent.source === "project").length +
143
+ potentialProjectFallbackAgents.length
144
+ : 0);
145
+ const discoveryOmissions =
146
+ (catalog.user.omittedAgentDefinitions ?? 0) +
147
+ (catalog.project?.omittedAgentDefinitions ?? 0) +
148
+ suppressedMetadataEntries;
149
+ const discoveryIncomplete =
150
+ catalog.user.metadataDiscoveryIncomplete === true ||
151
+ catalog.project?.metadataDiscoveryIncomplete === true;
152
+
153
+ const render = (entries: typeof allEntries, omitted: number): string => {
154
+ const lines = [
155
+ "Available agent definitions (metadata only; runtime validation and trust remain authoritative).",
156
+ ];
157
+ const userLines = entries
158
+ .filter((entry) => entry.scope === "user")
159
+ .map((entry) => catalogAgentLine(entry.agent, entry.scope, userNames, maxDescriptionLength));
160
+ if (userLines.length > 0) {
161
+ lines.push('Default scope (agentScope: "user"):');
162
+ lines.push(...userLines);
163
+ }
164
+ const projectLines = entries
165
+ .filter((entry) => entry.scope !== "user")
166
+ .map((entry) => catalogAgentLine(entry.agent, entry.scope, userNames, maxDescriptionLength));
167
+ if (projectLines.length > 0) {
168
+ lines.push("Trusted project/scope variants (use the required agentScope shown):");
169
+ lines.push(...projectLines);
170
+ }
171
+ const collisionNames = entries
172
+ .filter((entry) => entry.scope !== "user" && userNames.has(entry.agent.name))
173
+ .map((entry) => entry.agent.name);
174
+ if (collisionNames.length > 0 && projectLines.length > 0) {
175
+ const precedence = entries
176
+ .filter((entry) => entry.scope !== "user" && userNames.has(entry.agent.name))
177
+ .map((entry) =>
178
+ entry.scope === "project"
179
+ ? `${entry.agent.name}: user with "user", project with "project"/"both"`
180
+ : `${entry.agent.name}: user with "user"/"both", built-in with "project"`,
181
+ );
182
+ lines.push(`Same-name precedence: ${precedence.join("; ")}.`);
183
+ }
184
+ if (omitted > 0) {
185
+ lines.push(
186
+ `[${omitted} additional agent definition${omitted === 1 ? "" : "s"} omitted due to metadata bounds or incomplete discovery.]`,
187
+ );
188
+ }
189
+ if (discoveryIncomplete) {
190
+ lines.push("[Agent metadata discovery was incomplete; some definitions may be unavailable.]");
191
+ }
192
+ return lines.join("\n");
193
+ };
194
+
195
+ let listedCount = boundedEntries.length;
196
+ let text = render(
197
+ boundedEntries.slice(0, listedCount),
198
+ allEntries.length - listedCount + discoveryOmissions,
199
+ );
200
+ while (text.length > maxCharacters && listedCount > 0) {
201
+ listedCount -= 1;
202
+ text = render(
203
+ boundedEntries.slice(0, listedCount),
204
+ allEntries.length - listedCount + discoveryOmissions,
205
+ );
206
+ }
207
+ return { text, omitted: allEntries.length - listedCount + discoveryOmissions };
208
+ }
209
+
210
+ export function discoverAgentCatalog(
211
+ cwd: string,
212
+ projectTrusted: boolean,
213
+ config?: SubagentSettings,
214
+ ): AgentCatalog {
215
+ const options: AgentDiscoveryOptions = {
216
+ maxFiles: DEFAULT_AGENT_CATALOG_MAX_FILES_PER_SCOPE,
217
+ maxFileBytes: DEFAULT_AGENT_CATALOG_MAX_FILE_BYTES,
218
+ maxTotalBytes: DEFAULT_AGENT_CATALOG_MAX_TOTAL_BYTES_PER_SCOPE,
219
+ };
220
+ return {
221
+ user: discoverAgents(cwd, "user", config, options),
222
+ project: projectTrusted ? discoverAgents(cwd, "project", config, options) : undefined,
223
+ };
224
+ }