pi-orchestrator-config 4.1.5

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 (175) hide show
  1. package/README.md +808 -0
  2. package/agents/api-documenter.md +32 -0
  3. package/agents/bash-expert.md +53 -0
  4. package/agents/code-reviewer-docs.md +145 -0
  5. package/agents/code-reviewer-guidelines.md +97 -0
  6. package/agents/code-reviewer-quality.md +142 -0
  7. package/agents/code-reviewer-security.md +107 -0
  8. package/agents/code-reviewer-spec.md +164 -0
  9. package/agents/debugger.md +63 -0
  10. package/agents/docker-expert.md +40 -0
  11. package/agents/docs-fetcher.md +48 -0
  12. package/agents/git-expert.md +63 -0
  13. package/agents/github-expert.md +83 -0
  14. package/agents/go-expert.md +62 -0
  15. package/agents/issue-reviewer-feasibility.md +60 -0
  16. package/agents/issue-reviewer-scope.md +63 -0
  17. package/agents/issue-reviewer-spec.md +77 -0
  18. package/agents/java-expert.md +31 -0
  19. package/agents/jenkins-expert.md +38 -0
  20. package/agents/kubernetes-expert.md +40 -0
  21. package/agents/planner.md +42 -0
  22. package/agents/python-expert.md +61 -0
  23. package/agents/reviewer.md +26 -0
  24. package/agents/scout.md +40 -0
  25. package/agents/security-auditor.md +254 -0
  26. package/agents/technical-documentation-writer.md +37 -0
  27. package/agents/test-automator.md +55 -0
  28. package/agents/test-runner.md +47 -0
  29. package/agents/ts-expert.md +23 -0
  30. package/agents/worker.md +30 -0
  31. package/extensions/acpx-provider/index.ts +104 -0
  32. package/extensions/acpx-provider/load-runtime.ts +99 -0
  33. package/extensions/acpx-provider/runtime-models.ts +40 -0
  34. package/extensions/cli-provider/agents/claude.ts +128 -0
  35. package/extensions/cli-provider/agents/cursor.ts +84 -0
  36. package/extensions/cli-provider/agents/gemini.ts +100 -0
  37. package/extensions/cli-provider/discover.ts +77 -0
  38. package/extensions/cli-provider/index.ts +57 -0
  39. package/extensions/cli-provider/parsers.ts +304 -0
  40. package/extensions/cli-provider/providers.ts +58 -0
  41. package/extensions/cli-provider/runner.ts +225 -0
  42. package/extensions/cli-provider/runtime-models.ts +32 -0
  43. package/extensions/cli-provider/session-reaper.ts +122 -0
  44. package/extensions/cli-provider/sessions.ts +533 -0
  45. package/extensions/cli-provider/shared/discover-cache.ts +235 -0
  46. package/extensions/cli-provider/types.ts +48 -0
  47. package/extensions/coms/coms-net-server.ts +1642 -0
  48. package/extensions/coms/coms-net-wrapper.ts +521 -0
  49. package/extensions/coms/coms-net.ts +1589 -0
  50. package/extensions/coms/coms-p2p.ts +1879 -0
  51. package/extensions/coms/coms-shared.ts +510 -0
  52. package/extensions/coms/coms-wrapper.ts +138 -0
  53. package/extensions/coms/index.ts +20 -0
  54. package/extensions/coms/themeMap.ts +147 -0
  55. package/extensions/image-gen/image-gen.ts +332 -0
  56. package/extensions/image-gen/index.ts +14 -0
  57. package/extensions/orchestrator/agents.ts +134 -0
  58. package/extensions/orchestrator/ask-user.ts +293 -0
  59. package/extensions/orchestrator/async-agents.ts +1037 -0
  60. package/extensions/orchestrator/async-capability.ts +129 -0
  61. package/extensions/orchestrator/async-runner.ts +324 -0
  62. package/extensions/orchestrator/async-status-parse.ts +31 -0
  63. package/extensions/orchestrator/async-status-ui.ts +201 -0
  64. package/extensions/orchestrator/async-wait.ts +36 -0
  65. package/extensions/orchestrator/btw.ts +210 -0
  66. package/extensions/orchestrator/cron-status-format.ts +126 -0
  67. package/extensions/orchestrator/cron-status-ui.ts +132 -0
  68. package/extensions/orchestrator/cron.ts +730 -0
  69. package/extensions/orchestrator/dreaming.ts +355 -0
  70. package/extensions/orchestrator/enforcement-helpers.ts +530 -0
  71. package/extensions/orchestrator/enforcement-rules.ts +329 -0
  72. package/extensions/orchestrator/enforcement.ts +828 -0
  73. package/extensions/orchestrator/extended-autocomplete.ts +593 -0
  74. package/extensions/orchestrator/git-helpers.ts +420 -0
  75. package/extensions/orchestrator/github-autocomplete.ts +250 -0
  76. package/extensions/orchestrator/icons.ts +17 -0
  77. package/extensions/orchestrator/index.ts +100 -0
  78. package/extensions/orchestrator/memory-embeddings.ts +281 -0
  79. package/extensions/orchestrator/memory-promotion.ts +449 -0
  80. package/extensions/orchestrator/memory-provenance.ts +80 -0
  81. package/extensions/orchestrator/memory-query-class.ts +90 -0
  82. package/extensions/orchestrator/memory-scoring.ts +434 -0
  83. package/extensions/orchestrator/memory-tools.ts +883 -0
  84. package/extensions/orchestrator/memory-tree.ts +212 -0
  85. package/extensions/orchestrator/nvim.ts +169 -0
  86. package/extensions/orchestrator/overlay-dashboard-utils.ts +25 -0
  87. package/extensions/orchestrator/overlay-dashboard.ts +512 -0
  88. package/extensions/orchestrator/pi-config-review-state.ts +303 -0
  89. package/extensions/orchestrator/preference-extractor.ts +106 -0
  90. package/extensions/orchestrator/project-settings.ts +468 -0
  91. package/extensions/orchestrator/promotion-queue.ts +311 -0
  92. package/extensions/orchestrator/resolve-agent-model.ts +32 -0
  93. package/extensions/orchestrator/review-ui.ts +204 -0
  94. package/extensions/orchestrator/rule-placeholders.ts +12 -0
  95. package/extensions/orchestrator/rules.ts +481 -0
  96. package/extensions/orchestrator/session-search.ts +243 -0
  97. package/extensions/orchestrator/session-validation.ts +374 -0
  98. package/extensions/orchestrator/situation-report.ts +370 -0
  99. package/extensions/orchestrator/status-bar.ts +31 -0
  100. package/extensions/orchestrator/status-line.ts +192 -0
  101. package/extensions/orchestrator/status.ts +110 -0
  102. package/extensions/orchestrator/subagent-tool.ts +1748 -0
  103. package/extensions/orchestrator/task-lifecycle.ts +88 -0
  104. package/extensions/orchestrator/utils.ts +227 -0
  105. package/extensions/pidash/index.ts +15 -0
  106. package/extensions/pidash/pidash.ts +941 -0
  107. package/extensions/pidiff/index.ts +13 -0
  108. package/extensions/pidiff/pidiff.ts +369 -0
  109. package/extensions/providers/acpx-driver.ts +479 -0
  110. package/extensions/providers/built-in-drivers.ts +52 -0
  111. package/extensions/providers/claude-driver.ts +319 -0
  112. package/extensions/providers/cursor-acpx-driver.ts +398 -0
  113. package/extensions/providers/cursor-cli-driver.ts +484 -0
  114. package/extensions/providers/gemini-driver.ts +302 -0
  115. package/extensions/providers/index.ts +513 -0
  116. package/extensions/shared/build-system-prompt.ts +57 -0
  117. package/extensions/shared/create-runtime-provider.ts +190 -0
  118. package/extensions/shared/daemon-manager.ts +248 -0
  119. package/extensions/shared/file-logger.ts +115 -0
  120. package/extensions/shared/managed-refresh.ts +211 -0
  121. package/extensions/shared/provider-driver.ts +295 -0
  122. package/extensions/shared/provider-errors.ts +305 -0
  123. package/extensions/shared/provider-registry.ts +388 -0
  124. package/extensions/shared/stream-builder.ts +275 -0
  125. package/extensions/shared/types.ts +20 -0
  126. package/extensions/shared/ui/README.md +10 -0
  127. package/extensions/shared/ui/badge.tsx +52 -0
  128. package/extensions/shared/ui/button.tsx +58 -0
  129. package/extensions/shared/ui/collapsible.tsx +21 -0
  130. package/extensions/shared/ui/dropdown-menu.tsx +266 -0
  131. package/extensions/shared/ui/input.tsx +20 -0
  132. package/extensions/shared/ui/popover.tsx +114 -0
  133. package/extensions/shared/ui/resizable.tsx +48 -0
  134. package/extensions/shared/ui/scroll-area.tsx +53 -0
  135. package/extensions/shared/ui/select.tsx +201 -0
  136. package/extensions/shared/ui/separator.tsx +25 -0
  137. package/extensions/shared/ui/tooltip.tsx +64 -0
  138. package/extensions/shared/ws-client.ts +74 -0
  139. package/package.json +59 -0
  140. package/prompts/coderabbit-rate-limit.md +95 -0
  141. package/prompts/create-coms-feature-manager.md +186 -0
  142. package/prompts/create-skill.md +128 -0
  143. package/prompts/domain-model.md +109 -0
  144. package/prompts/external-ai.md +665 -0
  145. package/prompts/implement-and-review.md +25 -0
  146. package/prompts/implement.md +26 -0
  147. package/prompts/issue-review.md +256 -0
  148. package/prompts/pr-review.md +753 -0
  149. package/prompts/query-db.md +154 -0
  150. package/prompts/refine-review.md +208 -0
  151. package/prompts/release.md +235 -0
  152. package/prompts/remember.md +33 -0
  153. package/prompts/review-handler-status.md +27 -0
  154. package/prompts/review-handler.md +677 -0
  155. package/prompts/review-local.md +159 -0
  156. package/prompts/scout-and-plan.md +21 -0
  157. package/rules/00-orchestrator-core.md +42 -0
  158. package/rules/05-issue-first-workflow.md +119 -0
  159. package/rules/10-agent-routing.md +58 -0
  160. package/rules/15-mcp-launchpad.md +46 -0
  161. package/rules/20-code-review-loop.md +234 -0
  162. package/rules/25-documentation-updates.md +15 -0
  163. package/rules/30-prompt-templates.md +19 -0
  164. package/rules/35-memory.md +240 -0
  165. package/rules/40-critical-rules.md +124 -0
  166. package/rules/45-file-preview.md +29 -0
  167. package/rules/50-agent-bug-reporting.md +90 -0
  168. package/rules/55-coms-protocol.md +63 -0
  169. package/rules/60-task-tracking.md +66 -0
  170. package/skills/conflict-resolver/SKILL.md +213 -0
  171. package/skills/skill-creator/SKILL.md +85 -0
  172. package/skills/systematic-debugging/SKILL.md +353 -0
  173. package/skills/test-driven-development/SKILL.md +335 -0
  174. package/skills/update-agents-md/SKILL.md +282 -0
  175. package/templates/coms-feature-manager-prompt.md +313 -0
@@ -0,0 +1,513 @@
1
+ /**
2
+ * Unified Provider Extension for pi
3
+ *
4
+ * Single extension that replaces both cli-provider/index.ts and
5
+ * acpx-provider/index.ts. Uses the driver-based architecture inspired
6
+ * by t3code's ProviderDriver SPI.
7
+ *
8
+ * Registers via createProvider() (pi >= 0.81): /login, fetchModels,
9
+ * filterModels, and native ProviderStreams.
10
+ *
11
+ * How it works:
12
+ * 1. Read cli_agents and acpx_agents from settings
13
+ * 2. For each agent, find the matching driver from BUILT_IN_DRIVERS
14
+ * 3. Use ProviderDriverRegistry to probe + create instances
15
+ * 4. Register each instance as a pi provider via createRuntimeProvider
16
+ * 5. Unified session_start/shutdown/before_agent_start handlers
17
+ *
18
+ * Loaded from: extensions/providers/
19
+ */
20
+
21
+ import type {
22
+ AssistantMessage,
23
+ AssistantMessageEventStream,
24
+ Context,
25
+ Model,
26
+ SimpleStreamOptions,
27
+ StreamOptions,
28
+ } from "@earendil-works/pi-ai";
29
+ import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
30
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
31
+ import { asStringArray, getSetting } from "../orchestrator/project-settings.js";
32
+ import { buildExternalSystemPrompt } from "../shared/build-system-prompt.js";
33
+ import {
34
+ checkMinPiVersion,
35
+ isPiMetaInvocation,
36
+ } from "../orchestrator/utils.js";
37
+ import {
38
+ buildAmbientLoginAuth,
39
+ buildRuntimeModel,
40
+ createRuntimeProvider,
41
+ filterModelsWhenConfigured,
42
+ } from "../shared/create-runtime-provider.js";
43
+ import { StreamAssembler, createAssistantMessageOutput } from "../shared/stream-builder.js";
44
+ import { ProviderDriverRegistry } from "../shared/provider-registry.js";
45
+ import type { ProviderInstance, DiscoveredModel } from "../shared/provider-driver.js";
46
+ import { BUILT_IN_DRIVERS, CLI_AGENT_TO_DRIVER, ACPX_AGENT_TO_DRIVER } from "./built-in-drivers.js";
47
+ import { mapCliDiscoveredModels } from "../cli-provider/runtime-models.js";
48
+ import { mapAcpxDiscoveredModels } from "../acpx-provider/runtime-models.js";
49
+ import {
50
+ startCliSessionReaper,
51
+ stopCliSessionReaper,
52
+ } from "../cli-provider/session-reaper.js";
53
+ import {
54
+ readPiSessionIdFromManager,
55
+ } from "../cli-provider/sessions.js";
56
+ import { fileLog } from "../shared/file-logger.js";
57
+ import { isCliAgentName } from "../cli-provider/providers.js";
58
+
59
+ const LOG_DOMAIN = "providers";
60
+ const DISCOVERY_TIMEOUT_MS = 30_000;
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // State
64
+ // ---------------------------------------------------------------------------
65
+
66
+ const registry = new ProviderDriverRegistry();
67
+
68
+ /** Track which instances are CLI vs ACPX for configured gates. */
69
+ const cliInstances = new Map<string, ProviderInstance>();
70
+ const acpxInstances = new Map<string, ProviderInstance>();
71
+
72
+ let projectCwd = "";
73
+ let initialized = false;
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Configured Gates
77
+ // ---------------------------------------------------------------------------
78
+
79
+ function isCliInstanceConfigured(agent: string): boolean {
80
+ return cliInstances.has(agent);
81
+ }
82
+
83
+ function isAcpxInstanceConfigured(agent: string): boolean {
84
+ return acpxInstances.has(agent);
85
+ }
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // Context Helpers
89
+ // ---------------------------------------------------------------------------
90
+
91
+ function messageText(msg: { role: string; content: unknown }): string {
92
+ if (typeof msg.content === "string") return msg.content;
93
+ if (!Array.isArray(msg.content)) return "";
94
+ const textParts: string[] = [];
95
+ for (const block of msg.content) {
96
+ if ("text" in block && typeof block.text === "string") {
97
+ textParts.push(block.text);
98
+ }
99
+ }
100
+ return textParts.join("\n");
101
+ }
102
+
103
+ function extractLatestUserMessage(context: Context): string {
104
+ for (let i = context.messages.length - 1; i >= 0; i--) {
105
+ const msg = context.messages[i];
106
+ if (msg.role === "user") {
107
+ const text = messageText(msg);
108
+ if (text) return text;
109
+ }
110
+ }
111
+ return "hello";
112
+ }
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Stream Factory
116
+ // ---------------------------------------------------------------------------
117
+
118
+ function makeStreamFunction(
119
+ kind: "cli" | "acpx",
120
+ agent: string,
121
+ getInstance: () => ProviderInstance | undefined,
122
+ ) {
123
+ return function streamProvider(
124
+ model: Model<any>,
125
+ context: Context,
126
+ options?: SimpleStreamOptions | StreamOptions,
127
+ ): AssistantMessageEventStream {
128
+ const stream = createAssistantMessageEventStream();
129
+ const output = createAssistantMessageOutput(model);
130
+ const assembler = new StreamAssembler(output, stream);
131
+
132
+ (async () => {
133
+ try {
134
+ stream.push({ type: "start", partial: output });
135
+
136
+ const instance = getInstance();
137
+ if (!instance) {
138
+ throw new Error(`${kind}-${agent}: driver instance not available`);
139
+ }
140
+
141
+ // Parse model from pi model id: "agent:modelId"
142
+ const colonIdx = model.id.indexOf(":");
143
+ const driverModelId = colonIdx >= 0 ? model.id.substring(colonIdx + 1) : "default";
144
+
145
+ // Build system prompt
146
+ const systemPrompt = buildExternalSystemPrompt(context, projectCwd);
147
+
148
+ // Ensure session
149
+ const handle = await instance.adapter.startSession({
150
+ model: driverModelId,
151
+ systemPrompt,
152
+ cwd: projectCwd,
153
+ });
154
+
155
+ // Extract latest user message
156
+ const prompt = extractLatestUserMessage(context);
157
+
158
+ // Send turn with event bridging
159
+ const result = await instance.adapter.sendTurn(handle, prompt, {
160
+ signal: options?.signal,
161
+ context: { messages: context.messages },
162
+ onEvent: (event) => assembler.handleEvent(event),
163
+ });
164
+
165
+ assembler.finalize({
166
+ finalText: result.text,
167
+ finalThinking: result.thinking,
168
+ stopReason: result.stopReason,
169
+ usage: result.usage,
170
+ });
171
+ } catch (error) {
172
+ assembler.emitError(error, options?.signal?.aborted);
173
+ }
174
+ })();
175
+
176
+ return stream;
177
+ };
178
+ }
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // Registration Helpers
182
+ // ---------------------------------------------------------------------------
183
+
184
+ async function registerCliAgent(
185
+ pi: ExtensionAPI,
186
+ agent: string,
187
+ instance: ProviderInstance,
188
+ ): Promise<void> {
189
+ const providerId = `cli-${agent}`;
190
+ const snapshot = instance.snapshot.getSnapshot();
191
+ const discovered = snapshot.models;
192
+
193
+ const models = mapCliDiscoveredModels(agent, discovered);
194
+ const streamFn = makeStreamFunction("cli", agent, () => cliInstances.get(agent));
195
+
196
+ const provider = await createRuntimeProvider({
197
+ id: providerId,
198
+ name: `CLI ${agent}`,
199
+ auth: {
200
+ apiKey: buildAmbientLoginAuth({
201
+ displayName: `CLI ${agent}`,
202
+ isConfigured: () => isCliInstanceConfigured(agent),
203
+ sourceLabel: `${agent} CLI on PATH`,
204
+ }),
205
+ },
206
+ models,
207
+ fetchModels: async () => {
208
+ if (!isCliInstanceConfigured(agent)) return [];
209
+ const inst = cliInstances.get(agent);
210
+ if (!inst) return [];
211
+ const snap = await inst.snapshot.refresh();
212
+ return mapCliDiscoveredModels(agent, snap.models);
213
+ },
214
+ filterModels: (catalog, credential) =>
215
+ filterModelsWhenConfigured(catalog, credential, () =>
216
+ isCliInstanceConfigured(agent),
217
+ ),
218
+ api: { stream: streamFn, streamSimple: streamFn },
219
+ });
220
+ pi.registerProvider(provider);
221
+ fileLog(LOG_DOMAIN, "info", LOG_DOMAIN,
222
+ `cli-${agent}: ${models.length} model(s) registered`);
223
+ }
224
+
225
+ async function registerAcpxAgent(
226
+ pi: ExtensionAPI,
227
+ agent: string,
228
+ instance: ProviderInstance,
229
+ ): Promise<void> {
230
+ const providerId = `acpx-${agent}`;
231
+ const snapshot = instance.snapshot.getSnapshot();
232
+ const modelIds = snapshot.models.map((m) => m.id);
233
+
234
+ const models = mapAcpxDiscoveredModels(agent, modelIds);
235
+ const streamFn = makeStreamFunction("acpx", agent, () => acpxInstances.get(agent));
236
+
237
+ const provider = await createRuntimeProvider({
238
+ id: providerId,
239
+ name: `ACPX ${agent}`,
240
+ auth: {
241
+ apiKey: buildAmbientLoginAuth({
242
+ displayName: `ACPX ${agent}`,
243
+ isConfigured: () => isAcpxInstanceConfigured(agent),
244
+ sourceLabel: `${agent} acpx runtime`,
245
+ }),
246
+ },
247
+ models,
248
+ fetchModels: async () => {
249
+ const inst = acpxInstances.get(agent);
250
+ if (!inst) return [];
251
+ const snap = await inst.snapshot.refresh();
252
+ return mapAcpxDiscoveredModels(agent, snap.models.map((m) => m.id));
253
+ },
254
+ filterModels: (catalog, credential) =>
255
+ filterModelsWhenConfigured(catalog, credential, () =>
256
+ isAcpxInstanceConfigured(agent),
257
+ ),
258
+ api: { stream: streamFn, streamSimple: streamFn },
259
+ });
260
+ pi.registerProvider(provider);
261
+ fileLog(LOG_DOMAIN, "info", LOG_DOMAIN,
262
+ `acpx-${agent}: ${models.length} model(s) registered`);
263
+ }
264
+
265
+ // ---------------------------------------------------------------------------
266
+ // Extension Entry Point
267
+ // ---------------------------------------------------------------------------
268
+
269
+ export default async function (pi: ExtensionAPI) {
270
+ // pi --help / --version — skip discovery
271
+ if (isPiMetaInvocation()) return;
272
+
273
+ // Idempotency guard — shims re-export this default, so it may be called
274
+ // multiple times (cli-provider shim, acpx-provider shim, providers dir).
275
+ // Only run once per process.
276
+ if (initialized) return;
277
+ initialized = true;
278
+
279
+ const versionCheck = checkMinPiVersion();
280
+ if (!versionCheck.ok && versionCheck.installed !== null) {
281
+ fileLog(LOG_DOMAIN, "error", LOG_DOMAIN,
282
+ `pi ${versionCheck.installed} below minimum ${versionCheck.required}; skipping`);
283
+ return;
284
+ }
285
+
286
+ projectCwd = process.cwd();
287
+ const isSubagent = process.env.PI_SUBAGENT_CHILD === "1";
288
+
289
+ // Register all built-in drivers with the registry
290
+ for (const driver of BUILT_IN_DRIVERS) {
291
+ registry.registerDriver(driver);
292
+ }
293
+
294
+ // ---------------------------------------------------------------------------
295
+ // CLI agents (load in subagent children too)
296
+ // ---------------------------------------------------------------------------
297
+ const cliAgentList = asStringArray(getSetting(projectCwd, "cli_agents")).filter(isCliAgentName);
298
+
299
+ if (cliAgentList.length > 0) {
300
+ startCliSessionReaper({
301
+ cwd: projectCwd,
302
+ getActivePiSessionId: () => null, // Managed by driver adapters
303
+ });
304
+
305
+ // Bind pi session on before_agent_start (same as old cli-provider)
306
+ pi.on("before_agent_start", (_event, ctx) => {
307
+ const { readPiSessionId: sid } = readPiSessionIdFromManager(ctx.sessionManager);
308
+ if (sid) {
309
+ for (const inst of cliInstances.values()) {
310
+ const adapter = inst.adapter as any;
311
+ if (typeof adapter.bindPiSessionId === "function") {
312
+ adapter.bindPiSessionId(sid);
313
+ }
314
+ }
315
+ }
316
+ });
317
+
318
+ // Session lifecycle (same as old cli-provider)
319
+ pi.on("session_start", (event, ctx) => {
320
+ const reason = typeof (event as any)?.reason === "string"
321
+ ? (event as any).reason : "";
322
+ const { readPiSessionId: sid } = readPiSessionIdFromManager(ctx.sessionManager);
323
+
324
+ for (const inst of cliInstances.values()) {
325
+ const adapter = inst.adapter as any;
326
+ if (typeof adapter.handleSessionStart === "function") {
327
+ adapter.handleSessionStart(reason, sid);
328
+ }
329
+ }
330
+ });
331
+ }
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // Discovery — CLI and ACPX in parallel (on main these were separate
335
+ // extensions loaded concurrently by pi; keep the same parallelism)
336
+ // ---------------------------------------------------------------------------
337
+ async function discoverAndRegisterCli(): Promise<void> {
338
+ if (cliAgentList.length === 0) return;
339
+
340
+ const cliResults = await Promise.allSettled(
341
+ cliAgentList.map(async (agent) => {
342
+ const driverKind = CLI_AGENT_TO_DRIVER[agent];
343
+ if (!driverKind) {
344
+ fileLog(LOG_DOMAIN, "warn", LOG_DOMAIN,
345
+ `cli-${agent}: no driver registered`);
346
+ return null;
347
+ }
348
+
349
+ let timer: ReturnType<typeof setTimeout>;
350
+ let timedOut = false;
351
+ const timeout = new Promise<never>((_, reject) => {
352
+ timer = setTimeout(() => {
353
+ timedOut = true;
354
+ reject(new Error(`timed out after ${DISCOVERY_TIMEOUT_MS / 1000}s`));
355
+ }, DISCOVERY_TIMEOUT_MS);
356
+ if (timer.unref) timer.unref();
357
+ });
358
+
359
+ const discovery = (async () => {
360
+ try {
361
+ const instance = await registry.createInstance(
362
+ `cli-${agent}`,
363
+ { driver: driverKind, enabled: true },
364
+ projectCwd,
365
+ );
366
+ if (timedOut) return { agent, instance };
367
+ cliInstances.set(agent, instance);
368
+ return { agent, instance };
369
+ } catch (err) {
370
+ fileLog(LOG_DOMAIN, "warn", LOG_DOMAIN,
371
+ `cli-${agent}: driver create failed: ${err instanceof Error ? err.message : String(err)}`);
372
+ return null;
373
+ }
374
+ })();
375
+
376
+ // Clean up leaked instance if discovery completed after timeout
377
+ discovery.then((result) => {
378
+ if (timedOut && result) {
379
+ registry.teardownInstance(`cli-${agent}`).catch(() => {});
380
+ }
381
+ }).catch(() => {});
382
+
383
+ try {
384
+ const result = await Promise.race([discovery, timeout]);
385
+ clearTimeout(timer!);
386
+ return result;
387
+ } catch (err) {
388
+ clearTimeout(timer!);
389
+ fileLog(LOG_DOMAIN, "warn", LOG_DOMAIN,
390
+ `cli-${agent}: ${err instanceof Error ? err.message : String(err)}`);
391
+ return null;
392
+ }
393
+ }),
394
+ );
395
+
396
+ for (const result of cliResults) {
397
+ if (result.status !== "fulfilled" || !result.value) continue;
398
+ const { agent, instance } = result.value;
399
+ try {
400
+ await registerCliAgent(pi, agent, instance);
401
+ } catch (err) {
402
+ fileLog(LOG_DOMAIN, "error", LOG_DOMAIN,
403
+ `cli-${agent}: registration failed: ${err instanceof Error ? err.message : String(err)}`);
404
+ }
405
+ }
406
+ }
407
+
408
+ async function discoverAndRegisterAcpx(): Promise<void> {
409
+ if (isSubagent) return;
410
+ const acpxAgentList = asStringArray(getSetting(projectCwd, "acpx_agents"));
411
+ if (acpxAgentList.length === 0) return;
412
+
413
+ const acpxResults = await Promise.allSettled(
414
+ acpxAgentList.map(async (agent) => {
415
+ const driverKind = ACPX_AGENT_TO_DRIVER[agent] || "acpx";
416
+ // Only proceed if the driver is registered
417
+ if (!registry.getDriver(driverKind)) {
418
+ fileLog(LOG_DOMAIN, "warn", LOG_DOMAIN,
419
+ `acpx-${agent}: no driver '${driverKind}' registered`);
420
+ return null;
421
+ }
422
+
423
+ let timer: ReturnType<typeof setTimeout>;
424
+ let timedOut = false;
425
+ const timeout = new Promise<never>((_, reject) => {
426
+ timer = setTimeout(() => {
427
+ timedOut = true;
428
+ reject(new Error(`timed out after ${DISCOVERY_TIMEOUT_MS / 1000}s`));
429
+ }, DISCOVERY_TIMEOUT_MS);
430
+ if (timer.unref) timer.unref();
431
+ });
432
+
433
+ const discovery = (async () => {
434
+ try {
435
+ const instance = await registry.createInstance(
436
+ `acpx-${agent}`,
437
+ { driver: driverKind, config: { agent }, enabled: true },
438
+ projectCwd,
439
+ );
440
+ if (timedOut) return { agent, instance };
441
+ acpxInstances.set(agent, instance);
442
+ return { agent, instance };
443
+ } catch (err) {
444
+ fileLog(LOG_DOMAIN, "warn", LOG_DOMAIN,
445
+ `acpx-${agent}: driver create failed: ${err instanceof Error ? err.message : String(err)}`);
446
+ return null;
447
+ }
448
+ })();
449
+
450
+ // Clean up leaked instance if discovery completed after timeout
451
+ discovery.then((result) => {
452
+ if (timedOut && result) {
453
+ registry.teardownInstance(`acpx-${agent}`).catch(() => {});
454
+ }
455
+ }).catch(() => {});
456
+
457
+ try {
458
+ const result = await Promise.race([discovery, timeout]);
459
+ clearTimeout(timer!);
460
+ return result;
461
+ } catch (err) {
462
+ clearTimeout(timer!);
463
+ fileLog(LOG_DOMAIN, "warn", LOG_DOMAIN,
464
+ `acpx-${agent}: ${err instanceof Error ? err.message : String(err)}`);
465
+ return null;
466
+ }
467
+ }),
468
+ );
469
+
470
+ for (const result of acpxResults) {
471
+ if (result.status !== "fulfilled" || !result.value) continue;
472
+ const { agent, instance } = result.value;
473
+ try {
474
+ await registerAcpxAgent(pi, agent, instance);
475
+ } catch (err) {
476
+ fileLog(LOG_DOMAIN, "error", LOG_DOMAIN,
477
+ `acpx-${agent}: registration failed: ${err instanceof Error ? err.message : String(err)}`);
478
+ }
479
+ }
480
+ }
481
+
482
+ // Run CLI and ACPX discovery concurrently
483
+ await Promise.all([discoverAndRegisterCli(), discoverAndRegisterAcpx()]);
484
+
485
+ // Show discovery summary on session start
486
+ const providerSummaryParts: string[] = [];
487
+ for (const [agent, inst] of cliInstances) {
488
+ const count = inst.snapshot.getSnapshot().models.length;
489
+ providerSummaryParts.push(`cli-${agent} (${count})`);
490
+ }
491
+ for (const [agent, inst] of acpxInstances) {
492
+ const count = inst.snapshot.getSnapshot().models.length;
493
+ providerSummaryParts.push(`acpx-${agent} (${count})`);
494
+ }
495
+ if (providerSummaryParts.length > 0) {
496
+ pi.on("session_start", (_event, ctx) => {
497
+ if (!ctx.hasUI) return;
498
+ try {
499
+ ctx.ui.notify(`Providers: ${providerSummaryParts.join(", ")}`, "info");
500
+ } catch { /* stale ctx */ }
501
+ });
502
+ }
503
+
504
+ // ---------------------------------------------------------------------------
505
+ // Shutdown
506
+ // ---------------------------------------------------------------------------
507
+ pi.on("session_shutdown", async () => {
508
+ stopCliSessionReaper();
509
+ await registry.teardownAll();
510
+ cliInstances.clear();
511
+ acpxInstances.clear();
512
+ });
513
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Shared system prompt builder for CLI and ACPX providers.
3
+ * Injects enforced memory entries so external agents follow the same rules.
4
+ */
5
+
6
+ import { loadEnforcedEntries } from "../orchestrator/enforcement-rules.js";
7
+
8
+ interface ProviderContext {
9
+ systemPrompt?: string;
10
+ }
11
+
12
+ /**
13
+ * Build the system prompt for external LLM providers (CLI/ACPX).
14
+ * Appends enforced memory rules so external agents follow project enforcement.
15
+ */
16
+ export function buildExternalSystemPrompt(context: ProviderContext, cwd?: string): string | undefined {
17
+ if (!context.systemPrompt) return undefined;
18
+ const parts = [
19
+ "You are being used as a backend LLM through pi coding agent.",
20
+ "You have full permission to read, write, edit, and execute any files or commands.",
21
+ "Follow these instructions:",
22
+ "",
23
+ context.systemPrompt,
24
+ ];
25
+
26
+ // Inject enforced memory entries so CLI/ACPX agents follow the same rules
27
+ if (cwd) {
28
+ try {
29
+ const entries = loadEnforcedEntries(cwd);
30
+ if (entries.length > 0) {
31
+ const rules: string[] = [];
32
+ for (const e of entries) {
33
+ const trigger = e.trigger
34
+ .replace(/^bash_contains\s+/, "")
35
+ .replace(/^bash_regex\s+/, "")
36
+ .replace(/^tool_name\s+/, "")
37
+ .replace(/^file_modified\s+/, "");
38
+ if (e.action === "block") {
39
+ rules.push(`- NEVER: ${trigger} — ${e.text}`);
40
+ } else if (e.action === "warn") {
41
+ rules.push(`- WARNING when running \`${trigger}\`: ${e.text}`);
42
+ } else if (e.action.startsWith("run_after")) {
43
+ const cmd = e.actionCommand || e.action.slice("run_after ".length);
44
+ if (cmd) {
45
+ rules.push(`- After running \`${trigger}\`, ALWAYS run: \`${cmd}\``);
46
+ }
47
+ }
48
+ }
49
+ if (rules.length > 0) {
50
+ parts.push("", "## Enforced Rules (MANDATORY)", "", ...rules);
51
+ }
52
+ }
53
+ } catch { /* enforcement should never break the provider */ }
54
+ }
55
+
56
+ return parts.join("\n");
57
+ }