@ferris1225/pi-subagents 4.0.1 → 4.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/fixloop.ts CHANGED
@@ -1,16 +1,15 @@
1
1
  /**
2
- * Auto-fix loop: when a reviewer returns REVIEW_FAIL, the extension dispatches a
3
- * worker (briefed with the review's concrete findings) and then a reviewer
4
- * re-review, repeating up to maxFixRounds times before waking the main agent with
5
- * the full chain. The reviewer stays read-only and in its own context; the loop
6
- * is orchestrated by the extension layer, not by the reviewer itself, so the
7
- * independence guarantee (no self-confirmation bias) is preserved.
2
+ * Managed workflow policy and handoff formatting.
8
3
  *
9
- * The main agent is never woken mid-loop: the reviewer's FAIL result is intercepted
10
- * before delivery, the chain runs in the background, and only the final group
11
- * (initial review worker fixes re-reviews) is delivered at the end.
4
+ * Successful top-level writers can continue through documentation sync and an
5
+ * independent final review. A direct passing reviewer is also forced through
6
+ * documentation sync plus a fresh review when documenter is enabled. Any final
7
+ * gate failure may then use the established worker → optional documenter →
8
+ * reviewer fix rounds. Internal steps are launched by dispatch directly, so
9
+ * they never re-enter this top-level policy or wake the main agent mid-chain.
12
10
  */
13
11
 
12
+ import { isWriteCapableAgent, type AgentConfig } from "./agents.ts";
14
13
  import { getResultOutput, isFailedResult, reviewVerdict, type SingleResult } from "./spawn.ts";
15
14
  import { extractKeyFragments, formatUsageCompact, sumUsage } from "./monitor.ts";
16
15
  import type { SubagentsConfig } from "./config.ts";
@@ -31,12 +30,92 @@ export function shouldTriggerFixLoop(result: SingleResult, config: SubagentsConf
31
30
  // child happened to emit, which could end in a stray `VERDICT: REVIEW_FAIL`.
32
31
  // Guard explicitly in addition to isFailedResult so the intent is clear and
33
32
  // a future change to isFailedResult can never let a crashed reviewer start a
34
- // phantom auto-fix chain (and re-add a run controller id that was deleted in
35
- // the catch path).
33
+ // phantom auto-fix chain.
36
34
  if (result.dispatchFailed) return false;
37
35
  return reviewVerdict(getResultOutput(result)) === "fail";
38
36
  }
39
37
 
38
+ export interface WorkflowAgentAvailability {
39
+ worker: boolean;
40
+ cleaner: boolean;
41
+ documenter: boolean;
42
+ reviewer: boolean;
43
+ writer: boolean;
44
+ }
45
+
46
+ export function workflowAgentAvailability(
47
+ agents: readonly Pick<AgentConfig, "name" | "tools">[],
48
+ ): WorkflowAgentAvailability {
49
+ const names = new Set(agents.map((agent) => agent.name));
50
+ return {
51
+ worker: names.has("worker"),
52
+ cleaner: names.has("cleaner"),
53
+ documenter: names.has("documenter"),
54
+ reviewer: names.has("reviewer"),
55
+ writer: agents.some(isWriteCapableAgent),
56
+ };
57
+ }
58
+
59
+ export type ManagedWorkflowKind = "auto-fix" | "post-writer" | "review-pass-sync";
60
+
61
+ export interface ManagedWorkflowPlan {
62
+ kind: ManagedWorkflowKind;
63
+ initialRelation: string;
64
+ }
65
+
66
+ /** Conservative pre-run check used to reserve one shared-repository lane
67
+ * around a complete writer workflow or a reviewer that needs a stable diff.
68
+ * The actual result is classified again by getManagedWorkflowPlan before a
69
+ * downstream child starts. */
70
+ export function canStartManagedWorkflow(
71
+ agent: Pick<AgentConfig, "name" | "tools">,
72
+ availability: WorkflowAgentAvailability,
73
+ ): boolean {
74
+ // Every shared write-capable role—including custom agents—owns the repository
75
+ // lane even when no downstream role is enabled. Otherwise its edits can race
76
+ // a managed writer's documentation snapshot.
77
+ if (isWriteCapableAgent(agent)) return true;
78
+ if (agent.name === "reviewer") {
79
+ // Hold a stable diff snapshot against every discoverable writer even when
80
+ // this review is advisory or maxFixRounds=0. Classification happens only
81
+ // after the read-only child returns, too late to acquire the lane safely.
82
+ return availability.writer;
83
+ }
84
+ return false;
85
+ }
86
+
87
+ /** Classify only healthy top-level results. In particular, a reviewer without a
88
+ * machine verdict is advisory and cannot start any write-capable child. */
89
+ export function getManagedWorkflowPlan(
90
+ result: SingleResult,
91
+ config: SubagentsConfig,
92
+ availability: WorkflowAgentAvailability,
93
+ ): ManagedWorkflowPlan | undefined {
94
+ if (result.parked || result.dispatchFailed || isFailedResult(result)) return undefined;
95
+ if (result.agent === "worker" || result.agent === "cleaner") {
96
+ if (!availability.documenter && !availability.reviewer) return undefined;
97
+ return {
98
+ kind: "post-writer",
99
+ initialRelation: result.agent === "cleaner" ? "initial cleanup" : "initial implementation",
100
+ };
101
+ }
102
+ if (result.agent === "documenter") {
103
+ return availability.reviewer
104
+ ? { kind: "post-writer", initialRelation: "documentation pass" }
105
+ : undefined;
106
+ }
107
+ if (result.agent !== "reviewer") return undefined;
108
+
109
+ const verdict = reviewVerdict(getResultOutput(result));
110
+ if (verdict === "pass" && availability.documenter && availability.reviewer) {
111
+ return { kind: "review-pass-sync", initialRelation: "pre-documentation review" };
112
+ }
113
+ if (verdict === "fail" && availability.worker && shouldTriggerFixLoop(result, config)) {
114
+ return { kind: "auto-fix", initialRelation: "initial review" };
115
+ }
116
+ return undefined;
117
+ }
118
+
40
119
  /**
41
120
  * Build the worker task brief for one fix round from a reviewer's findings.
42
121
  * The worker gets the full review text so it can address concrete file:line
@@ -56,20 +135,83 @@ export function buildFixTaskBrief(reviewerResult: SingleResult, round: number, m
56
135
  `Fix EVERY finding in the reviewer's findings list — there is no severity triage; all of them get fixed.`,
57
136
  `If a finding is factually wrong or clearly out of scope, say so explicitly instead of fixing it.`,
58
137
  `Do NOT refactor unrelated code beyond what the findings require.`,
138
+ `Do NOT commit, push, publish, tag, or release; do not bump versions. The parent chain still owns documentation sync and final review.`,
59
139
  `After editing, run the project's format/build/tests when they exist and report`,
60
140
  `exactly what you changed (paths + short rationale) so a reviewer can verify.`,
61
141
  remaining > 0
62
142
  ? `A reviewer will re-review your changes automatically after you finish.`
63
- : `This is the last auto-fix round; the main agent will be woken with the full chain.`,
143
+ : `This is the last auto-fix round; optional documentation sync and a fresh reviewer still run before final delivery.`,
64
144
  ].join("\n");
65
145
  }
66
146
 
147
+ interface DocumentationBriefOptions {
148
+ title: string;
149
+ reports: Array<{ label: string; result: SingleResult }>;
150
+ closing: string;
151
+ }
152
+
153
+ function buildDocumentationBrief(options: DocumentationBriefOptions): string {
154
+ const reportSections = options.reports.flatMap(({ label, result }) => [
155
+ `${label}:`,
156
+ `---`,
157
+ getResultOutput(result),
158
+ `---`,
159
+ ``,
160
+ ]);
161
+ return [
162
+ options.title,
163
+ ``,
164
+ ...reportSections,
165
+ `Inspect the actual git diff (the complete pending diff) and relevant implementation; the report is only a lead.`,
166
+ `Synchronize stale README/docs, examples, API comments, docstrings, and explanatory comments with the behavior that will be committed.`,
167
+ `Change documentation surfaces only; never alter runtime behavior or tests to make prose true.`,
168
+ `Make zero edits when the diff creates no documentation drift.`,
169
+ `Do NOT commit, push, publish, tag, or release; do not bump versions. ${options.closing}`,
170
+ `Report exact documentation/comment paths changed, or state explicitly that no sync was needed.`,
171
+ ].join("\n");
172
+ }
173
+
174
+ /** Build the pre-commit documentation handoff after one auto-fix worker. */
175
+ export function buildDocumenterTaskBrief(
176
+ workerResult: SingleResult,
177
+ round: number,
178
+ reviewerResult?: SingleResult,
179
+ ): string {
180
+ return buildDocumentationBrief({
181
+ title: `Documentation sync after auto-fix round ${round}.`,
182
+ reports: [
183
+ ...(reviewerResult ? [{ label: "The triggering reviewer reported", result: reviewerResult }] : []),
184
+ { label: "The worker reported", result: workerResult },
185
+ ],
186
+ closing: "a fresh reviewer gate runs after you.",
187
+ });
188
+ }
189
+
190
+ /** Build the automatic documentation stage after a successful top-level writer. */
191
+ export function buildPostWriterDocumenterBrief(writerResult: SingleResult): string {
192
+ return buildDocumentationBrief({
193
+ title: `Documentation sync after successful top-level ${writerResult.agent}.`,
194
+ reports: [{ label: `The ${writerResult.agent} reported`, result: writerResult }],
195
+ closing: "the managed workflow owns any final reviewer and delivery.",
196
+ });
197
+ }
198
+
199
+ /** A direct passing review cannot be the final gate while documenter is enabled:
200
+ * the preliminary report is context, but the actual pending diff is authoritative. */
201
+ export function buildReviewPassDocumenterBrief(reviewerResult: SingleResult): string {
202
+ return buildDocumentationBrief({
203
+ title: "Documentation sync required before accepting a direct passing review.",
204
+ reports: [{ label: "The preliminary reviewer reported", result: reviewerResult }],
205
+ closing: "the preliminary pass is not final and a fresh reviewer gate runs after you.",
206
+ });
207
+ }
208
+
67
209
  /**
68
210
  * One step of an auto-fix chain as delivered: the run id (so the condensed
69
211
  * summary can point at per-run detail via subagent_status), the result, and
70
212
  * the human-readable role within the chain ("initial review", "fix round 1",
71
- * "re-review round 2"). runId is undefined for steps that never spawned a run
72
- * (e.g. an unknown agent).
213
+ * "re-review round 2"). runId is optional only for synthetic steps that never
214
+ * spawned a child.
73
215
  */
74
216
  export interface ChainStep {
75
217
  runId?: number;
@@ -77,6 +219,11 @@ export interface ChainStep {
77
219
  relation: string;
78
220
  }
79
221
 
222
+ export interface ManagedWorkflowOutcome {
223
+ kind: ManagedWorkflowKind;
224
+ steps: ChainStep[];
225
+ }
226
+
80
227
  /** Max distinguishing fragments kept in a one-line chain summary. */
81
228
  export const CHAIN_SUMMARY_FRAGMENTS_MAX = 3;
82
229
 
@@ -87,58 +234,124 @@ export function chainKeyFragments(result: SingleResult): string[] {
87
234
  return extractKeyFragments(getResultOutput(result)).slice(0, CHAIN_SUMMARY_FRAGMENTS_MAX);
88
235
  }
89
236
 
90
- /**
91
- * Condensed, readable summary of a completed auto-fix chain: one line per step
92
- * (run id, role, verdict / what changed) plus aggregate usage. Full per-step
93
- * reports stay addressable via `subagent_status <id>`; the caller appends the
94
- * final step's full block only when its detail is actionable (FAIL verdict or a
95
- * crash), so the delivered message stays short instead of stacking every
96
- * round's raw output.
97
- */
98
- export function formatChainSummary(steps: readonly ChainStep[]): string {
99
- const rounds = steps.filter((step) => step.relation.startsWith("fix round")).length;
100
- const last = steps[steps.length - 1];
101
- const stepStatus = (step: ChainStep): string => {
102
- const { result } = step;
103
- if (result.agent === "reviewer") {
104
- const verdict = reviewVerdict(getResultOutput(result));
105
- if (verdict) return verdict.toUpperCase();
106
- }
107
- return isFailedResult(result) ? "failed" : "completed";
108
- };
109
- const lines = [
110
- `## Auto-fix chain: ${Math.max(1, rounds)} round${rounds === 1 ? "" : "s"} — final ${stepStatus(last)}`,
111
- "",
112
- ];
113
- for (const step of steps) {
114
- const fragments = chainKeyFragments(step.result);
115
- const suffix =
116
- fragments.length > 0
117
- ? step.result.agent === "worker"
118
- ? ` — changed: ${fragments.join(" · ")}`
119
- : ` — ${fragments.join(" · ")}`
120
- : "";
121
- const id = step.runId !== undefined ? `#${step.runId} ` : "";
122
- lines.push(`- ${id}${step.result.agent} · ${step.relation} · ${stepStatus(step)}${suffix}`);
237
+ function workflowResultStatus(result: SingleResult): string {
238
+ if (isFailedResult(result)) return "failed";
239
+ if (result.agent === "reviewer") {
240
+ const verdict = reviewVerdict(getResultOutput(result));
241
+ return verdict ? verdict.toUpperCase() : "NO_VERDICT";
123
242
  }
243
+ return "completed";
244
+ }
245
+
246
+ function workflowStepLine(step: ChainStep): string {
247
+ const fragments = chainKeyFragments(step.result);
248
+ const writer = step.result.agent === "worker" || step.result.agent === "cleaner" || step.result.agent === "documenter";
249
+ const suffix = fragments.length > 0
250
+ ? writer
251
+ ? ` — changed: ${fragments.join(" · ")}`
252
+ : ` — ${fragments.join(" · ")}`
253
+ : "";
254
+ const id = step.runId !== undefined ? `#${step.runId} ` : "";
255
+ return `- ${id}${step.result.agent} · ${step.relation} · ${workflowResultStatus(step.result)}${suffix}`;
256
+ }
257
+
258
+ function appendWorkflowFooter(lines: string[], steps: readonly ChainStep[]): void {
124
259
  const total = sumUsage(steps.map((step) => step.result.usage));
125
260
  const usage = formatUsageCompact(total);
126
261
  lines.push("", `Totals: ${steps.length} run${steps.length === 1 ? "" : "s"}${usage ? ` · ${usage}` : ""}`);
127
262
  const ids = steps.filter((step) => step.runId !== undefined).map((step) => `#${step.runId}`);
128
263
  lines.push(`Full per-run reports (output, usage, failed tools): subagent_status ${ids.join(" ")}`);
264
+ }
265
+
266
+ /** Condensed compatibility summary for a direct REVIEW_FAIL auto-fix chain. */
267
+ export function formatChainSummary(
268
+ steps: readonly ChainStep[],
269
+ terminalResult: SingleResult = steps[steps.length - 1]!.result,
270
+ ): string {
271
+ const rounds = steps.filter((step) => step.relation.startsWith("fix round")).length;
272
+ const lines = [
273
+ `## Auto-fix chain: ${Math.max(1, rounds)} round${rounds === 1 ? "" : "s"} — final ${workflowResultStatus(terminalResult)}`,
274
+ "",
275
+ ...steps.map(workflowStepLine),
276
+ ];
277
+ appendWorkflowFooter(lines, steps);
278
+ return lines.join("\n");
279
+ }
280
+
281
+ /** One clear final delivery for all newly managed writer/documenter workflows. */
282
+ export function formatManagedWorkflowSummary(
283
+ steps: readonly ChainStep[],
284
+ terminalResult: SingleResult = steps[steps.length - 1]!.result,
285
+ ): string {
286
+ const route = steps.map((step) => step.result.agent).join(" → ");
287
+ const fixRounds = steps.filter((step) => step.relation.startsWith("fix round")).length;
288
+ const roundNote = fixRounds > 0 ? ` · ${fixRounds} fix round${fixRounds === 1 ? "" : "s"}` : "";
289
+ const lines = [
290
+ `## Managed workflow: ${route}${roundNote} — final ${workflowResultStatus(terminalResult)}`,
291
+ "",
292
+ ...steps.map(workflowStepLine),
293
+ ];
294
+ appendWorkflowFooter(lines, steps);
129
295
  return lines.join("\n");
130
296
  }
131
297
 
298
+ /** Build the first independent final gate after a top-level writer or required
299
+ * post-pass documentation sync. Reports carry intent; the actual pending diff
300
+ * remains authoritative. */
301
+ export function buildFinalReviewBrief(
302
+ initialResult: SingleResult,
303
+ documenterResult?: SingleResult,
304
+ ): string {
305
+ const documenterSection = documenterResult
306
+ ? [
307
+ ``,
308
+ `The documenter's full sync report:`,
309
+ `---`,
310
+ getResultOutput(documenterResult),
311
+ `---`,
312
+ ]
313
+ : [];
314
+ return [
315
+ `Fresh final gate for a managed ${initialResult.agent} workflow.`,
316
+ ``,
317
+ `The top-level ${initialResult.agent}'s full report:`,
318
+ `---`,
319
+ getResultOutput(initialResult),
320
+ `---`,
321
+ ...documenterSection,
322
+ ``,
323
+ `Run \`git status\` and \`git diff\` and inspect the actual pending code and documentation; reports are context, not proof.`,
324
+ `Remain read-only. Verify correctness, regressions, tests, documentation drift, and that documenter was the last writer when it ran.`,
325
+ `This is an acceptance gate, not an advisory audit. End with exactly one standalone machine verdict line:`,
326
+ `VERDICT: REVIEW_PASS when no finding remains, otherwise VERDICT: REVIEW_FAIL.`,
327
+ ].join("\n");
328
+ }
329
+
132
330
  /**
133
331
  * The re-review brief handed to the reviewer after a worker fix round. Includes
134
- * the prior review AND the worker's report so the reviewer can adjudicate
135
- * rejections instead of restating findings. The convergence contract keeps
136
- * rounds from ping-ponging: rule on the open findings once, add only defects
137
- * this round's edits introduced, never re-open a verified resolution.
332
+ * the prior review, worker report, and optional documenter report so the
333
+ * reviewer can adjudicate rejections instead of restating findings. The
334
+ * convergence contract keeps rounds from ping-ponging: rule on the open
335
+ * findings once, add only defects this round's edits introduced, never re-open
336
+ * a verified resolution.
138
337
  */
139
- export function buildReReviewBrief(reviewerResult: SingleResult, round: number, workerResult: SingleResult): string {
338
+ export function buildReReviewBrief(
339
+ reviewerResult: SingleResult,
340
+ round: number,
341
+ workerResult: SingleResult,
342
+ documenterResult?: SingleResult,
343
+ ): string {
140
344
  const review = getResultOutput(reviewerResult);
141
345
  const workerReport = getResultOutput(workerResult);
346
+ const documenterSection = documenterResult
347
+ ? [
348
+ ``,
349
+ `The documenter's pre-commit sync report:`,
350
+ `---`,
351
+ getResultOutput(documenterResult),
352
+ `---`,
353
+ ]
354
+ : [];
142
355
  return [
143
356
  `Re-review after auto-fix round ${round}.`,
144
357
  ``,
@@ -151,6 +364,7 @@ export function buildReReviewBrief(reviewerResult: SingleResult, round: number,
151
364
  `---`,
152
365
  workerReport,
153
366
  `---`,
367
+ ...documenterSection,
154
368
  ``,
155
369
  `Rule on EVERY previous finding: resolved, or still open. A finding the worker rejected must be`,
156
370
  `adjudicated ONCE — accept the rejection unless you can concretely refute the worker's reasoning;`,
package/src/index.ts CHANGED
@@ -1,93 +1,93 @@
1
- /**
2
- * pi-subagents — focused sub-agent delegation for pi.
3
- *
4
- * Assembly point: builds the shared runtime and registers everything.
5
- * The heavy lifting lives in focused modules:
6
- * - dispatch.ts — the `subagent` tool contract and auto-fix chain
7
- * - thread-lifecycle.ts — queued generations, resume/fork, isolation settlement
8
- * - tools.ts — subagent_control / subagent_wait / status / stop
9
- * - announcements.ts — session-start recovery, notices, and widget install
10
- * - widget.ts — active-only TUI run status
11
- * - runtime.ts — shared per-session state
12
- *
13
- * Also registers the `/subagents-setup` command and a `before_agent_start` hook
14
- * that injects a delegation directive into the parent system prompt so the main
15
- * model uses the tool proactively.
16
- *
17
- * The tool is not registered inside child sub-agent processes, which prevents
18
- * runaway recursion and keeps child context windows clean.
19
- */
20
-
21
- import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
22
- import { Text } from "@earendil-works/pi-tui";
23
- import { discoverAgents } from "./agents.ts";
24
- import { registerAnnouncements } from "./announcements.ts";
25
- import { getConfigPath, loadConfig } from "./config.ts";
26
- import { registerSubagentTool } from "./dispatch.ts";
27
- import { matchRunIds } from "./format.ts";
28
- import { buildDelegationDirective } from "./prompt.ts";
29
- import { createRuntime } from "./runtime.ts";
30
- import { runSetup } from "./setup.ts";
31
- import { currentSubagentDepth } from "./spawn.ts";
32
- import { registerLookupTools } from "./tools.ts";
33
- import { clearActiveRunsWidget } from "./widget.ts";
34
-
35
- export { matchRunIds };
36
-
37
- export default function (pi: ExtensionAPI): void {
38
- const configPath = getConfigPath(getAgentDir());
39
- const runtime = createRuntime(pi, configPath);
40
-
41
- // Recursion guard: sub-agent children are leaf processes. The `subagent` tool is
42
- // excluded from their toolset at spawn (--exclude-tools); this check is defense
43
- // in depth so a child can never expose the tool back to its model, even if
44
- // another extension ignores the depth marker.
45
- if (currentSubagentDepth() >= 1) {
46
- pi.registerCommand("subagents-setup", {
47
- description: "Configure pi-subagents (unavailable in nested sub-agent processes)",
48
- handler: async (_args, ctx) => {
49
- ctx.ui.notify("pi-subagents setup is unavailable in nested sub-agent processes.", "warning");
50
- },
51
- });
52
- return;
53
- }
54
-
55
- pi.registerMessageRenderer("subagent-result", (message, _options, theme) =>
56
- new Text(
57
- `${theme.fg("toolTitle", theme.bold("subagent result"))}\n${message.content}`,
58
- 0,
59
- 0,
60
- ),
61
- );
62
-
63
- pi.on("session_shutdown", async (_event, ctx) => {
64
- clearActiveRunsWidget(ctx);
65
- await runtime.shutdown();
66
- });
67
-
68
- registerSubagentTool(pi, runtime);
69
- registerLookupTools(pi, runtime);
70
-
71
- pi.registerCommand("subagents-setup", {
72
- description: "Configure pi-subagents: agents, selected models, capability-aware thinking, and runtime settings",
73
- handler: async (_args, ctx) => {
74
- await runSetup(ctx, configPath);
75
- },
76
- });
77
-
78
- registerAnnouncements(pi, runtime);
79
-
80
- // Proactive dispatch: inject the delegation directive into the parent system prompt.
81
- pi.on("before_agent_start", async (event, ctx) => {
82
- const config = await loadConfig(configPath);
83
- if (!config.proactiveInjection) return undefined;
84
- const { agents } = discoverAgents(ctx.cwd, {
85
- scope: config.agentScope,
86
- enabledNames: config.enabledAgents,
87
- projectTrusted: ctx.isProjectTrusted?.() === true,
88
- });
89
- const directive = buildDelegationDirective(agents);
90
- if (!directive) return undefined;
91
- return { systemPrompt: `${event.systemPrompt}\n${directive}` };
92
- });
93
- }
1
+ /**
2
+ * pi-subagents — focused sub-agent delegation for pi.
3
+ *
4
+ * Assembly point: builds the shared runtime and registers everything.
5
+ * The heavy lifting lives in focused modules:
6
+ * - dispatch.ts — tool contract, managed role policy, internal steps
7
+ * - thread-lifecycle.ts — stable generations, controls, final integration/delivery
8
+ * - tools.ts — subagent_control / subagent_wait / status / stop
9
+ * - announcements.ts — session-start recovery, notices, and widget install
10
+ * - widget.ts — active-only TUI run status
11
+ * - runtime.ts — shared per-session state
12
+ *
13
+ * Also registers the `/subagents-setup` command and a `before_agent_start` hook
14
+ * that injects a delegation directive into the parent system prompt so the main
15
+ * model uses the tool proactively.
16
+ *
17
+ * The tool is not registered inside child sub-agent processes, which prevents
18
+ * runaway recursion and keeps child context windows clean.
19
+ */
20
+
21
+ import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
22
+ import { Text } from "@earendil-works/pi-tui";
23
+ import { discoverAgents } from "./agents.ts";
24
+ import { registerAnnouncements } from "./announcements.ts";
25
+ import { getConfigPath, loadConfig } from "./config.ts";
26
+ import { registerSubagentTool } from "./dispatch.ts";
27
+ import { matchRunIds } from "./format.ts";
28
+ import { buildDelegationDirective } from "./prompt.ts";
29
+ import { createRuntime } from "./runtime.ts";
30
+ import { runSetup } from "./setup.ts";
31
+ import { currentSubagentDepth } from "./spawn.ts";
32
+ import { registerLookupTools } from "./tools.ts";
33
+ import { clearActiveRunsWidget } from "./widget.ts";
34
+
35
+ export { matchRunIds };
36
+
37
+ export default function (pi: ExtensionAPI): void {
38
+ const configPath = getConfigPath(getAgentDir());
39
+ const runtime = createRuntime(pi, configPath);
40
+
41
+ // Recursion guard: sub-agent children are leaf processes. The `subagent` tool is
42
+ // excluded from their toolset at spawn (--exclude-tools); this check is defense
43
+ // in depth so a child can never expose the tool back to its model, even if
44
+ // another extension ignores the depth marker.
45
+ if (currentSubagentDepth() >= 1) {
46
+ pi.registerCommand("subagents-setup", {
47
+ description: "Configure pi-subagents (unavailable in nested sub-agent processes)",
48
+ handler: async (_args, ctx) => {
49
+ ctx.ui.notify("pi-subagents setup is unavailable in nested sub-agent processes.", "warning");
50
+ },
51
+ });
52
+ return;
53
+ }
54
+
55
+ pi.registerMessageRenderer("subagent-result", (message, _options, theme) =>
56
+ new Text(
57
+ `${theme.fg("toolTitle", theme.bold("subagent result"))}\n${message.content}`,
58
+ 0,
59
+ 0,
60
+ ),
61
+ );
62
+
63
+ pi.on("session_shutdown", async (_event, ctx) => {
64
+ clearActiveRunsWidget(ctx);
65
+ await runtime.shutdown();
66
+ });
67
+
68
+ registerSubagentTool(pi, runtime);
69
+ registerLookupTools(pi, runtime);
70
+
71
+ pi.registerCommand("subagents-setup", {
72
+ description: "Configure pi-subagents: agents, selected models, capability-aware thinking, and runtime settings",
73
+ handler: async (_args, ctx) => {
74
+ await runSetup(ctx, configPath);
75
+ },
76
+ });
77
+
78
+ registerAnnouncements(pi, runtime);
79
+
80
+ // Proactive dispatch: inject the delegation directive into the parent system prompt.
81
+ pi.on("before_agent_start", async (event, ctx) => {
82
+ const config = await loadConfig(configPath);
83
+ if (!config.proactiveInjection) return undefined;
84
+ const { agents } = discoverAgents(ctx.cwd, {
85
+ scope: config.agentScope,
86
+ enabledNames: config.enabledAgents,
87
+ projectTrusted: ctx.isProjectTrusted?.() === true,
88
+ });
89
+ const directive = buildDelegationDirective(agents, { maxFixRounds: config.maxFixRounds });
90
+ if (!directive) return undefined;
91
+ return { systemPrompt: `${event.systemPrompt}\n${directive}` };
92
+ });
93
+ }