@narumitw/pi-subagents 0.53.0 → 0.54.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 (64) hide show
  1. package/README.md +75 -6
  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 +2 -3
  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 +35 -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 +1 -1
  32. package/src/registry-types.ts +1 -1
  33. package/src/registry.ts +1 -1
  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 +1 -1
  46. package/src/stateful-limits.ts +1 -1
  47. package/src/stateful-prompt.ts +2 -2
  48. package/src/stateful-safety.ts +2 -1
  49. package/src/stateful.ts +23 -103
  50. package/src/subagents.ts +8 -9
  51. package/src/subprocess-transport.ts +2 -6
  52. package/src/transport-types.ts +1 -1
  53. package/src/transport-ui.ts +1 -1
  54. package/src/verification-harness.ts +516 -0
  55. package/src/verification-receipt.ts +275 -0
  56. package/src/verified-execution-benchmark.ts +86 -0
  57. package/src/verified-execution-contract.ts +219 -0
  58. package/src/work-item-ledger.ts +510 -37
  59. package/src/work-item-persistence.ts +31 -0
  60. package/src/workflow-completion-controller.ts +397 -0
  61. package/src/workflow-plan-compiler.ts +1 -1
  62. package/src/workflow-plan-patch.ts +1 -1
  63. package/src/workflow-planning.ts +11 -1
  64. package/src/workflow-ui.ts +1 -1
package/README.md CHANGED
@@ -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,10 +576,26 @@ 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
 
@@ -1010,7 +1074,12 @@ packages/pi-subagents/
1010
1074
  │ ├── execution-plan.ts # Executor-owned authority and resource resolution
1011
1075
  │ ├── work-item-ledger.ts # Persistent dependency and artifact state machine
1012
1076
  │ ├── work-item-persistence.ts # Atomic redacted workflow state and inspection
1013
- │ ├── workflow-verification.ts # Executor-owned independent-verifier receipts
1077
+ │ ├── workflow-verification.ts # Compatibility independent-verifier receipts
1078
+ │ ├── verified-execution-contract.ts # Explicit managed-verification request boundary
1079
+ │ ├── workflow-completion-controller.ts # Sole opted-in terminal acceptance owner
1080
+ │ ├── verification-harness.ts # Disposable deterministic check execution
1081
+ │ ├── verification-receipt.ts # Strict executor-owned managed receipts
1082
+ │ ├── verified-execution-benchmark.ts # Matched offline acceptance/cost fixture
1014
1083
  │ ├── workflow-tree-identity.ts # Bounded exact Git-visible tree identities
1015
1084
  │ ├── integration-controller.ts # Fail-closed canonical integration admission
1016
1085
  │ ├── 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": "0.54.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
+ }