@leing2021/super-pi 0.22.1 → 0.23.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 (110) hide show
  1. package/README.md +253 -211
  2. package/extensions/ce-core/index.ts +190 -7
  3. package/extensions/ce-core/tools/parallel-subagent.ts +3 -3
  4. package/extensions/ce-core/tools/subagent.ts +2 -2
  5. package/package.json +4 -20
  6. package/rules/common/naming.md +154 -0
  7. package/skills/02-plan/SKILL.md +3 -2
  8. package/skills/03-work/SKILL.md +9 -6
  9. package/skills/04-review/SKILL.md +3 -2
  10. package/skills/06-next/SKILL.md +38 -10
  11. package/skills/06-next/references/recommendation-logic.md +2 -2
  12. package/skills/07-worktree/SKILL.md +9 -8
  13. package/skills/{09-help → 08-help}/SKILL.md +3 -3
  14. package/skills/{09-help → 08-help}/references/workflow-sequence.md +2 -1
  15. package/skills/references/language-detection.md +44 -0
  16. package/LICENSE +0 -21
  17. package/THIRD-PARTY-NOTICES.md +0 -35
  18. package/extensions/subagent/__tests__/async-job-tracker.test.ts +0 -21
  19. package/extensions/subagent/__tests__/execution-activity.test.ts +0 -15
  20. package/extensions/subagent/__tests__/parallel-render-stress.test.ts +0 -82
  21. package/extensions/subagent/__tests__/render-dedup.test.ts +0 -98
  22. package/extensions/subagent/__tests__/render-widget.test.ts +0 -28
  23. package/extensions/subagent/__tests__/throttle.test.ts +0 -90
  24. package/extensions/subagent/agent-management.ts +0 -596
  25. package/extensions/subagent/agent-manager-chain-detail.ts +0 -163
  26. package/extensions/subagent/agent-manager-detail.ts +0 -232
  27. package/extensions/subagent/agent-manager-edit.ts +0 -391
  28. package/extensions/subagent/agent-manager-list.ts +0 -279
  29. package/extensions/subagent/agent-manager-parallel.ts +0 -305
  30. package/extensions/subagent/agent-manager.ts +0 -706
  31. package/extensions/subagent/agent-scope.ts +0 -9
  32. package/extensions/subagent/agent-selection.ts +0 -26
  33. package/extensions/subagent/agent-serializer.ts +0 -124
  34. package/extensions/subagent/agent-templates.ts +0 -63
  35. package/extensions/subagent/agents/context-builder.md +0 -37
  36. package/extensions/subagent/agents/delegate.md +0 -9
  37. package/extensions/subagent/agents/oracle.md +0 -73
  38. package/extensions/subagent/agents/planner.md +0 -52
  39. package/extensions/subagent/agents/researcher.md +0 -50
  40. package/extensions/subagent/agents/reviewer.md +0 -38
  41. package/extensions/subagent/agents/scout.md +0 -48
  42. package/extensions/subagent/agents/worker.md +0 -52
  43. package/extensions/subagent/agents.ts +0 -762
  44. package/extensions/subagent/artifacts.ts +0 -101
  45. package/extensions/subagent/async-execution.ts +0 -521
  46. package/extensions/subagent/async-job-tracker.ts +0 -240
  47. package/extensions/subagent/async-status.ts +0 -242
  48. package/extensions/subagent/chain-clarify.ts +0 -1365
  49. package/extensions/subagent/chain-execution.ts +0 -854
  50. package/extensions/subagent/chain-serializer.ts +0 -127
  51. package/extensions/subagent/completion-dedupe.ts +0 -66
  52. package/extensions/subagent/doctor.ts +0 -201
  53. package/extensions/subagent/execution.ts +0 -748
  54. package/extensions/subagent/file-coalescer.ts +0 -43
  55. package/extensions/subagent/fork-context.ts +0 -64
  56. package/extensions/subagent/formatters.ts +0 -123
  57. package/extensions/subagent/frontmatter.ts +0 -32
  58. package/extensions/subagent/index.ts +0 -586
  59. package/extensions/subagent/intercom-bridge.ts +0 -241
  60. package/extensions/subagent/jsonl-writer.ts +0 -84
  61. package/extensions/subagent/model-fallback.ts +0 -109
  62. package/extensions/subagent/notify.ts +0 -111
  63. package/extensions/subagent/parallel-utils.ts +0 -109
  64. package/extensions/subagent/pi-args.ts +0 -139
  65. package/extensions/subagent/pi-spawn.ts +0 -101
  66. package/extensions/subagent/post-exit-stdio-guard.ts +0 -88
  67. package/extensions/subagent/prompt-template-bridge.ts +0 -400
  68. package/extensions/subagent/prompts/gather-context-and-clarify.md +0 -13
  69. package/extensions/subagent/prompts/parallel-cleanup.md +0 -42
  70. package/extensions/subagent/prompts/parallel-research.md +0 -50
  71. package/extensions/subagent/prompts/parallel-review.md +0 -40
  72. package/extensions/subagent/render-helpers.ts +0 -83
  73. package/extensions/subagent/render.ts +0 -908
  74. package/extensions/subagent/result-intercom.ts +0 -238
  75. package/extensions/subagent/result-watcher.ts +0 -172
  76. package/extensions/subagent/run-history.ts +0 -58
  77. package/extensions/subagent/run-status.ts +0 -137
  78. package/extensions/subagent/schemas.ts +0 -165
  79. package/extensions/subagent/session-tokens.ts +0 -51
  80. package/extensions/subagent/settings.ts +0 -368
  81. package/extensions/subagent/single-output.ts +0 -98
  82. package/extensions/subagent/skills.ts +0 -627
  83. package/extensions/subagent/slash-bridge.ts +0 -177
  84. package/extensions/subagent/slash-commands.ts +0 -304
  85. package/extensions/subagent/slash-live-state.ts +0 -295
  86. package/extensions/subagent/subagent-control.ts +0 -151
  87. package/extensions/subagent/subagent-executor.ts +0 -1915
  88. package/extensions/subagent/subagent-prompt-runtime.ts +0 -76
  89. package/extensions/subagent/subagent-runner.ts +0 -1471
  90. package/extensions/subagent/subagents-status.ts +0 -473
  91. package/extensions/subagent/text-editor.ts +0 -273
  92. package/extensions/subagent/throttle.ts +0 -77
  93. package/extensions/subagent/top-level-async.ts +0 -16
  94. package/extensions/subagent/types.ts +0 -624
  95. package/extensions/subagent/utils.ts +0 -457
  96. package/extensions/subagent/worktree.ts +0 -580
  97. package/extensions/super-pi-extension/agents/ce-oracle.md +0 -109
  98. package/extensions/super-pi-extension/agents/ce-planner.md +0 -116
  99. package/extensions/super-pi-extension/agents/ce-reviewer.md +0 -99
  100. package/extensions/super-pi-extension/agents/ce-scout.md +0 -80
  101. package/extensions/super-pi-extension/agents/ce-worker.md +0 -78
  102. package/extensions/super-pi-extension/chains/ce-parallel-review.chain.md +0 -34
  103. package/extensions/super-pi-extension/chains/ce-review-only.chain.md +0 -12
  104. package/extensions/super-pi-extension/chains/ce-standard.chain.md +0 -28
  105. package/extensions/super-pi-extension/index.ts +0 -129
  106. package/extensions/super-pi-extension/model-sync.ts +0 -204
  107. package/skills/08-status/SKILL.md +0 -41
  108. package/skills/08-status/references/artifact-locations.md +0 -10
  109. package/skills/10-rules/SKILL.md +0 -76
  110. package/skills/pi-subagents/SKILL.md +0 -566
@@ -1,241 +0,0 @@
1
- // SPDX-FileCopyrightText: 2025 Nico Bailon
2
- // SPDX-License-Identifier: MIT
3
- // Source: https://github.com/nicobailon/pi-subagents
4
- import * as fs from "node:fs";
5
- import * as os from "node:os";
6
- import * as path from "node:path";
7
- import type { AgentConfig } from "./agents.ts";
8
- import type { ExtensionConfig, IntercomBridgeConfig, IntercomBridgeMode } from "./types.ts";
9
-
10
- const DEFAULT_INTERCOM_EXTENSION_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", "pi-intercom");
11
- const DEFAULT_INTERCOM_CONFIG_PATH = path.join(os.homedir(), ".pi", "agent", "intercom", "config.json");
12
- const DEFAULT_SUBAGENT_CONFIG_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", "subagent");
13
- const DEFAULT_INTERCOM_TARGET_PREFIX = "subagent-chat";
14
- export const INTERCOM_BRIDGE_MARKER = "Intercom orchestration channel:";
15
- const DEFAULT_INTERCOM_BRIDGE_TEMPLATE = `The inherited thread is reference-only. Do not continue that conversation or send questions, status updates, or completion handoffs to the orchestrator in normal assistant text.
16
-
17
- Use intercom only for coordination with the orchestrator session "{orchestratorTarget}".
18
- - Need a decision or blocked: intercom({ action: "ask", to: "{orchestratorTarget}", message: "<question>" })
19
- - Blocked or explicitly asked to send progress: intercom({ action: "send", to: "{orchestratorTarget}", message: "UPDATE: <summary>" })
20
-
21
- Do not send routine completion handoffs through intercom. If no coordination is needed, return a focused task result.`;
22
-
23
- export interface IntercomBridgeState {
24
- active: boolean;
25
- mode: IntercomBridgeMode;
26
- orchestratorTarget?: string;
27
- extensionDir: string;
28
- instruction: string;
29
- }
30
-
31
- export interface IntercomBridgeDiagnostic {
32
- active: boolean;
33
- mode: IntercomBridgeMode;
34
- wantsIntercom: boolean;
35
- piIntercomAvailable: boolean;
36
- extensionDir: string;
37
- configPath?: string;
38
- orchestratorTarget?: string;
39
- reason?: string;
40
- intercomConfigEnabled?: boolean;
41
- intercomConfigError?: string;
42
- }
43
-
44
- interface ResolveIntercomBridgeInput {
45
- config: ExtensionConfig["intercomBridge"];
46
- context: "fresh" | "fork" | undefined;
47
- orchestratorTarget?: string;
48
- extensionDir?: string;
49
- configPath?: string;
50
- settingsDir?: string;
51
- }
52
-
53
- export function resolveIntercomSessionTarget(sessionName: string | undefined, sessionId: string): string {
54
- const trimmedName = sessionName?.trim();
55
- if (trimmedName) return trimmedName;
56
- const normalizedSessionId = sessionId.startsWith("session-") ? sessionId.slice("session-".length) : sessionId;
57
- return `${DEFAULT_INTERCOM_TARGET_PREFIX}-${normalizedSessionId.slice(0, 8)}`;
58
- }
59
-
60
- function sanitizeIntercomTargetPart(value: string): string {
61
- return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
62
- }
63
-
64
- export function resolveSubagentIntercomTarget(runId: string, agent: string, index?: number): string {
65
- const stepSuffix = index !== undefined ? `-${index + 1}` : "";
66
- return `subagent-${sanitizeIntercomTargetPart(agent)}-${sanitizeIntercomTargetPart(runId)}${stepSuffix}`;
67
- }
68
-
69
- export function resolveIntercomBridgeMode(value: unknown): IntercomBridgeMode {
70
- if (value === "off" || value === "always" || value === "fork-only") return value;
71
- return "always";
72
- }
73
-
74
- function resolveIntercomBridgeConfig(value: ExtensionConfig["intercomBridge"]): Required<IntercomBridgeConfig> {
75
- if (!value || typeof value !== "object" || Array.isArray(value)) {
76
- return {
77
- mode: "always",
78
- instructionFile: "",
79
- };
80
- }
81
- return {
82
- mode: resolveIntercomBridgeMode(value.mode),
83
- instructionFile: typeof value.instructionFile === "string" ? value.instructionFile : "",
84
- };
85
- }
86
-
87
- function intercomConfigStatus(configPath: string): { enabled: boolean; error?: unknown } {
88
- if (!fs.existsSync(configPath)) return { enabled: true };
89
- try {
90
- const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8")) as { enabled?: unknown };
91
- return { enabled: parsed.enabled !== false };
92
- } catch (error) {
93
- return { enabled: true, error };
94
- }
95
- }
96
-
97
- function extensionSandboxAllowsIntercom(extensions: string[] | undefined, extensionDir: string): boolean {
98
- if (extensions === undefined) return true;
99
-
100
- const intercomDir = path.resolve(extensionDir).replaceAll("\\", "/").toLowerCase();
101
- for (const entry of extensions) {
102
- const normalized = entry.trim().replaceAll("\\", "/").toLowerCase();
103
- if (normalized === "pi-intercom") return true;
104
- if (normalized === intercomDir) return true;
105
- if (normalized.startsWith(`${intercomDir}/`)) return true;
106
- if (normalized.endsWith("/pi-intercom")) return true;
107
- if (normalized.includes("/pi-intercom/")) return true;
108
- }
109
- return false;
110
- }
111
-
112
- function expandTilde(filePath: string): string {
113
- return filePath.startsWith("~/") ? path.join(os.homedir(), filePath.slice(2)) : filePath;
114
- }
115
-
116
- function resolveInstructionTemplate(instructionFile: string, settingsDir: string): string {
117
- if (!instructionFile) return DEFAULT_INTERCOM_BRIDGE_TEMPLATE;
118
- const expandedPath = expandTilde(instructionFile);
119
- const resolvedPath = path.isAbsolute(expandedPath)
120
- ? expandedPath
121
- : path.resolve(settingsDir, expandedPath);
122
- try {
123
- return fs.readFileSync(resolvedPath, "utf-8");
124
- } catch (error) {
125
- console.warn(`Failed to read intercom bridge instructionFile at '${resolvedPath}'. Using default instructions.`, error);
126
- return DEFAULT_INTERCOM_BRIDGE_TEMPLATE;
127
- }
128
- }
129
-
130
- function buildIntercomBridgeInstruction(orchestratorTarget: string, template: string): string {
131
- const instruction = template.replaceAll("{orchestratorTarget}", orchestratorTarget).trim();
132
- if (instruction.startsWith(INTERCOM_BRIDGE_MARKER)) return instruction;
133
- return `${INTERCOM_BRIDGE_MARKER}
134
- ${instruction}`;
135
- }
136
-
137
- export function diagnoseIntercomBridge(input: ResolveIntercomBridgeInput): IntercomBridgeDiagnostic {
138
- const config = resolveIntercomBridgeConfig(input.config);
139
- const mode = config.mode;
140
- const extensionDir = path.resolve(input.extensionDir ?? DEFAULT_INTERCOM_EXTENSION_DIR);
141
- const orchestratorTarget = input.orchestratorTarget?.trim();
142
- const configPath = path.resolve(input.configPath ?? DEFAULT_INTERCOM_CONFIG_PATH);
143
- const wantsIntercom = mode !== "off" && !(mode === "fork-only" && input.context !== "fork");
144
- const piIntercomAvailable = fs.existsSync(extensionDir);
145
- let configStatus: ReturnType<typeof intercomConfigStatus> | undefined;
146
- let reason: string | undefined;
147
- if (mode === "off") reason = "bridge mode is off";
148
- else if (mode === "fork-only" && input.context !== "fork") reason = "bridge mode is fork-only and context is not fork";
149
- else if (!orchestratorTarget) reason = "orchestrator target is not available";
150
- else if (!piIntercomAvailable) reason = "pi-intercom extension was not found";
151
- else {
152
- configStatus = intercomConfigStatus(configPath);
153
- if (!configStatus.enabled) reason = "intercom config is disabled";
154
- }
155
- let intercomConfigError: string | undefined;
156
- if (configStatus?.error) {
157
- const error = configStatus.error;
158
- intercomConfigError = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
159
- }
160
-
161
- return {
162
- active: reason === undefined,
163
- mode,
164
- wantsIntercom,
165
- piIntercomAvailable,
166
- extensionDir,
167
- configPath,
168
- ...(orchestratorTarget ? { orchestratorTarget } : {}),
169
- ...(reason ? { reason } : {}),
170
- ...(configStatus ? { intercomConfigEnabled: configStatus.enabled } : {}),
171
- ...(intercomConfigError ? { intercomConfigError } : {}),
172
- };
173
- }
174
-
175
- export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): IntercomBridgeState {
176
- const config = resolveIntercomBridgeConfig(input.config);
177
- const mode = config.mode;
178
- const extensionDir = path.resolve(input.extensionDir ?? DEFAULT_INTERCOM_EXTENSION_DIR);
179
- const orchestratorTarget = input.orchestratorTarget?.trim();
180
- const settingsDir = path.resolve(input.settingsDir ?? DEFAULT_SUBAGENT_CONFIG_DIR);
181
- const defaultInstruction = buildIntercomBridgeInstruction(
182
- orchestratorTarget || "{orchestratorTarget}",
183
- DEFAULT_INTERCOM_BRIDGE_TEMPLATE,
184
- );
185
-
186
- if (mode === "off") {
187
- return { active: false, mode, extensionDir, instruction: defaultInstruction };
188
- }
189
- if (mode === "fork-only" && input.context !== "fork") {
190
- return { active: false, mode, extensionDir, instruction: defaultInstruction };
191
- }
192
- if (!orchestratorTarget) {
193
- return { active: false, mode, extensionDir, instruction: defaultInstruction };
194
- }
195
- if (!fs.existsSync(extensionDir)) {
196
- return { active: false, mode, extensionDir, instruction: defaultInstruction };
197
- }
198
-
199
- const configPath = path.resolve(input.configPath ?? DEFAULT_INTERCOM_CONFIG_PATH);
200
- const intercomStatus = intercomConfigStatus(configPath);
201
- if (intercomStatus.error) console.warn(`Failed to parse intercom config at '${configPath}'. Assuming enabled.`, intercomStatus.error);
202
- if (!intercomStatus.enabled) {
203
- return { active: false, mode, extensionDir, instruction: defaultInstruction };
204
- }
205
-
206
- const instruction = buildIntercomBridgeInstruction(
207
- orchestratorTarget,
208
- resolveInstructionTemplate(config.instructionFile, settingsDir),
209
- );
210
-
211
- return {
212
- active: true,
213
- mode,
214
- orchestratorTarget,
215
- extensionDir,
216
- instruction,
217
- };
218
- }
219
-
220
- export function applyIntercomBridgeToAgent(agent: AgentConfig, bridge: IntercomBridgeState): AgentConfig {
221
- if (!bridge.active || !bridge.orchestratorTarget) return agent;
222
- if (!extensionSandboxAllowsIntercom(agent.extensions, bridge.extensionDir)) return agent;
223
-
224
- const tools = agent.tools && !agent.tools.includes("intercom")
225
- ? [...agent.tools, "intercom"]
226
- : agent.tools;
227
- const instruction = bridge.instruction;
228
- const trimmedPrompt = agent.systemPrompt?.trim() || "";
229
- const systemPrompt = trimmedPrompt.includes(INTERCOM_BRIDGE_MARKER)
230
- ? trimmedPrompt
231
- : trimmedPrompt
232
- ? `${trimmedPrompt}\n\n${instruction}`
233
- : instruction;
234
-
235
- if (tools === agent.tools && systemPrompt === agent.systemPrompt) return agent;
236
- return {
237
- ...agent,
238
- tools,
239
- systemPrompt,
240
- };
241
- }
@@ -1,84 +0,0 @@
1
- // SPDX-FileCopyrightText: 2025 Nico Bailon
2
- // SPDX-License-Identifier: MIT
3
- // Source: https://github.com/nicobailon/pi-subagents
4
- import * as fs from "node:fs";
5
-
6
- export interface DrainableSource {
7
- pause(): void;
8
- resume(): void;
9
- }
10
-
11
- export interface JsonlWriteStream {
12
- write(chunk: string): boolean;
13
- once(event: "drain", listener: () => void): JsonlWriteStream;
14
- end(callback?: () => void): void;
15
- }
16
-
17
- const DEFAULT_MAX_JSONL_BYTES = 50 * 1024 * 1024;
18
-
19
- export interface JsonlWriterDeps {
20
- createWriteStream?: (filePath: string) => JsonlWriteStream;
21
- maxBytes?: number;
22
- }
23
-
24
- export interface JsonlWriter {
25
- writeLine(line: string): void;
26
- close(): Promise<void>;
27
- }
28
-
29
- export function createJsonlWriter(
30
- filePath: string | undefined,
31
- source: DrainableSource,
32
- deps: JsonlWriterDeps = {},
33
- ): JsonlWriter {
34
- if (!filePath) {
35
- return {
36
- writeLine() {},
37
- async close() {},
38
- };
39
- }
40
-
41
- const createWriteStream = deps.createWriteStream ?? ((targetPath: string) => fs.createWriteStream(targetPath, { flags: "a" }));
42
- let stream: JsonlWriteStream | undefined;
43
- try {
44
- stream = createWriteStream(filePath);
45
- } catch {
46
- return {
47
- writeLine() {},
48
- async close() {},
49
- };
50
- }
51
-
52
- let backpressured = false;
53
- let closed = false;
54
- let bytesWritten = 0;
55
- const maxBytes = deps.maxBytes ?? DEFAULT_MAX_JSONL_BYTES;
56
-
57
- return {
58
- writeLine(line: string) {
59
- if (!stream || closed || !line.trim()) return;
60
- const chunk = `${line}\n`;
61
- const chunkBytes = Buffer.byteLength(chunk, "utf-8");
62
- if (bytesWritten + chunkBytes > maxBytes) return;
63
- try {
64
- const ok = stream.write(chunk);
65
- bytesWritten += chunkBytes;
66
- if (!ok && !backpressured) {
67
- backpressured = true;
68
- source.pause();
69
- stream.once("drain", () => {
70
- backpressured = false;
71
- if (!closed) source.resume();
72
- });
73
- }
74
- } catch {}
75
- },
76
- async close() {
77
- if (!stream || closed) return;
78
- closed = true;
79
- const current = stream;
80
- stream = undefined;
81
- await new Promise<void>((resolve) => current.end(() => resolve()));
82
- },
83
- };
84
- }
@@ -1,109 +0,0 @@
1
- // SPDX-FileCopyrightText: 2025 Nico Bailon
2
- // SPDX-License-Identifier: MIT
3
- // Source: https://github.com/nicobailon/pi-subagents
4
- import type { Usage } from "./types.ts";
5
-
6
- export interface AvailableModelInfo {
7
- provider: string;
8
- id: string;
9
- fullId: string;
10
- }
11
-
12
- export interface ModelAttemptSummary {
13
- model: string;
14
- success: boolean;
15
- exitCode?: number | null;
16
- error?: string;
17
- usage?: Usage;
18
- }
19
-
20
- export function splitThinkingSuffix(model: string): { baseModel: string; thinkingSuffix: string } {
21
- const colonIdx = model.lastIndexOf(":");
22
- if (colonIdx === -1) return { baseModel: model, thinkingSuffix: "" };
23
- return {
24
- baseModel: model.substring(0, colonIdx),
25
- thinkingSuffix: model.substring(colonIdx),
26
- };
27
- }
28
-
29
- export function resolveModelCandidate(
30
- model: string | undefined,
31
- availableModels: AvailableModelInfo[] | undefined,
32
- preferredProvider?: string,
33
- ): string | undefined {
34
- if (!model) return undefined;
35
- if (model.includes("/")) return model;
36
- if (!availableModels || availableModels.length === 0) return model;
37
-
38
- const { baseModel, thinkingSuffix } = splitThinkingSuffix(model);
39
- const matches = availableModels.filter((entry) => entry.id === baseModel);
40
- if (preferredProvider) {
41
- const preferredMatch = matches.find((entry) => entry.provider === preferredProvider);
42
- if (preferredMatch) return `${preferredMatch.fullId}${thinkingSuffix}`;
43
- }
44
- if (matches.length !== 1) return model;
45
- return `${matches[0]!.fullId}${thinkingSuffix}`;
46
- }
47
-
48
- export function buildModelCandidates(
49
- primaryModel: string | undefined,
50
- fallbackModels: string[] | undefined,
51
- availableModels: AvailableModelInfo[] | undefined,
52
- preferredProvider?: string,
53
- ): string[] {
54
- const seen = new Set<string>();
55
- const candidates: string[] = [];
56
- for (const raw of [primaryModel, ...(fallbackModels ?? [])]) {
57
- if (!raw) continue;
58
- const normalized = resolveModelCandidate(raw.trim(), availableModels, preferredProvider);
59
- if (!normalized || seen.has(normalized)) continue;
60
- seen.add(normalized);
61
- candidates.push(normalized);
62
- }
63
- return candidates;
64
- }
65
-
66
- const RETRYABLE_MODEL_FAILURE_PATTERNS = [
67
- /rate\s*limit/i,
68
- /too many requests/i,
69
- /\b429\b/,
70
- /quota/i,
71
- /billing/i,
72
- /credit/i,
73
- /auth(?:entication)?/i,
74
- /unauthori[sz]ed/i,
75
- /forbidden/i,
76
- /api key/i,
77
- /token expired/i,
78
- /invalid key/i,
79
- /provider.*unavailable/i,
80
- /model.*unavailable/i,
81
- /model.*disabled/i,
82
- /model.*not found/i,
83
- /unknown model/i,
84
- /overloaded/i,
85
- /service unavailable/i,
86
- /temporar(?:ily)? unavailable/i,
87
- /connection refused/i,
88
- /fetch failed/i,
89
- /network error/i,
90
- /socket hang up/i,
91
- /upstream/i,
92
- /timed? out/i,
93
- /timeout/i,
94
- /\b502\b/,
95
- /\b503\b/,
96
- /\b504\b/,
97
- ];
98
-
99
- export function isRetryableModelFailure(error: string | undefined): boolean {
100
- if (!error) return false;
101
- return RETRYABLE_MODEL_FAILURE_PATTERNS.some((pattern) => pattern.test(error));
102
- }
103
-
104
- export function formatModelAttemptNote(attempt: ModelAttemptSummary, nextModel?: string): string {
105
- const failure = attempt.error?.trim() || `exit ${attempt.exitCode ?? 1}`;
106
- return nextModel
107
- ? `[fallback] ${attempt.model} failed: ${failure}. Retrying with ${nextModel}.`
108
- : `[fallback] ${attempt.model} failed: ${failure}.`;
109
- }
@@ -1,111 +0,0 @@
1
- // SPDX-FileCopyrightText: 2025 Nico Bailon
2
- // SPDX-License-Identifier: MIT
3
- // Source: https://github.com/nicobailon/pi-subagents
4
- /**
5
- * Subagent completion notifications.
6
- */
7
-
8
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
- import { buildCompletionKey, getGlobalSeenMap, markSeenWithTtl } from "./completion-dedupe.ts";
10
- import { SUBAGENT_ASYNC_COMPLETE_EVENT } from "./types.ts";
11
-
12
- interface ChainStepResult {
13
- agent: string;
14
- output: string;
15
- success: boolean;
16
- }
17
-
18
- export interface SubagentNotifyDetails {
19
- agent: string;
20
- status: "completed" | "failed" | "paused";
21
- taskInfo?: string;
22
- resultPreview: string;
23
- durationMs?: number;
24
- sessionLabel?: string;
25
- sessionValue?: string;
26
- }
27
-
28
- interface SubagentResult {
29
- id: string | null;
30
- agent: string | null;
31
- success: boolean;
32
- summary: string;
33
- exitCode?: number;
34
- state?: string;
35
- timestamp: number;
36
- durationMs?: number;
37
- sessionFile?: string;
38
- shareUrl?: string;
39
- gistUrl?: string;
40
- shareError?: string;
41
- results?: ChainStepResult[];
42
- taskIndex?: number;
43
- totalTasks?: number;
44
- }
45
-
46
- export default function registerSubagentNotify(pi: ExtensionAPI): void {
47
- const unsubscribeStoreKey = "__pi_subagents_notify_unsubscribe__";
48
- const globalStore = globalThis as Record<string, unknown>;
49
- const previousUnsubscribe = globalStore[unsubscribeStoreKey];
50
- if (typeof previousUnsubscribe === "function") {
51
- try {
52
- previousUnsubscribe();
53
- } catch {
54
- // Best effort cleanup for stale handlers from an older reload.
55
- }
56
- }
57
-
58
- const seen = getGlobalSeenMap("__pi_subagents_notify_seen__");
59
- const ttlMs = 10 * 60 * 1000;
60
-
61
- const handleComplete = (data: unknown) => {
62
- const result = data as SubagentResult;
63
- const now = Date.now();
64
- const key = buildCompletionKey(result, "notify");
65
- if (markSeenWithTtl(seen, key, now, ttlMs)) return;
66
-
67
- const agent = result.agent ?? "unknown";
68
- const summary = typeof result.summary === "string" ? result.summary : "";
69
- const paused = !result.success && (
70
- result.exitCode === 0
71
- || result.state === "paused"
72
- || summary.startsWith("Paused after interrupt.")
73
- );
74
- const status = paused ? "paused" : result.success ? "completed" : "failed";
75
-
76
- const taskInfo =
77
- result.taskIndex !== undefined && result.totalTasks !== undefined
78
- ? ` (${result.taskIndex + 1}/${result.totalTasks})`
79
- : "";
80
-
81
- const sessionLine = result.shareUrl
82
- ? `Session: ${result.shareUrl}`
83
- : result.shareError
84
- ? `Session share error: ${result.shareError}`
85
- : result.sessionFile
86
- ? `Session file: ${result.sessionFile}`
87
- : undefined;
88
-
89
- const displaySummary = summary.trim() ? summary : "(no output)";
90
- const content = [
91
- `Background task ${status}: **${agent}**${taskInfo}`,
92
- "",
93
- displaySummary,
94
- sessionLine ? "" : undefined,
95
- sessionLine,
96
- ]
97
- .filter((line) => line !== undefined)
98
- .join("\n");
99
-
100
- pi.sendMessage(
101
- {
102
- customType: "subagent-notify",
103
- content,
104
- display: true,
105
- },
106
- { triggerTurn: true },
107
- );
108
- };
109
-
110
- globalStore[unsubscribeStoreKey] = pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, handleComplete);
111
- }
@@ -1,109 +0,0 @@
1
- // SPDX-FileCopyrightText: 2025 Nico Bailon
2
- // SPDX-License-Identifier: MIT
3
- // Source: https://github.com/nicobailon/pi-subagents
4
- export interface RunnerSubagentStep {
5
- agent: string;
6
- task: string;
7
- cwd?: string;
8
- model?: string;
9
- modelCandidates?: string[];
10
- tools?: string[];
11
- extensions?: string[];
12
- mcpDirectTools?: string[];
13
- systemPrompt?: string | null;
14
- systemPromptMode?: "append" | "replace";
15
- inheritProjectContext: boolean;
16
- inheritSkills: boolean;
17
- skills?: string[];
18
- outputPath?: string;
19
- sessionFile?: string;
20
- maxSubagentDepth?: number;
21
- }
22
-
23
- export interface ParallelStepGroup {
24
- parallel: RunnerSubagentStep[];
25
- concurrency?: number;
26
- failFast?: boolean;
27
- worktree?: boolean;
28
- }
29
-
30
- export type RunnerStep = RunnerSubagentStep | ParallelStepGroup;
31
-
32
- export function isParallelGroup(step: RunnerStep): step is ParallelStepGroup {
33
- return "parallel" in step && Array.isArray(step.parallel);
34
- }
35
-
36
- export function flattenSteps(steps: RunnerStep[]): RunnerSubagentStep[] {
37
- const flat: RunnerSubagentStep[] = [];
38
- for (const step of steps) {
39
- if (isParallelGroup(step)) {
40
- for (const task of step.parallel) flat.push(task);
41
- } else {
42
- flat.push(step);
43
- }
44
- }
45
- return flat;
46
- }
47
-
48
- export async function mapConcurrent<T, R>(
49
- items: T[],
50
- limit: number,
51
- fn: (item: T, i: number) => Promise<R>,
52
- ): Promise<R[]> {
53
- const safeLimit = Math.max(1, Math.floor(limit) || 1);
54
- const results: R[] = new Array(items.length);
55
- let next = 0;
56
-
57
- async function worker(_workerIndex: number): Promise<void> {
58
- while (next < items.length) {
59
- const i = next++;
60
- results[i] = await fn(items[i], i);
61
- }
62
- }
63
-
64
- await Promise.all(
65
- Array.from({ length: Math.min(safeLimit, items.length) }, (_, wi) => worker(wi)),
66
- );
67
- return results;
68
- }
69
-
70
- export interface ParallelTaskResult {
71
- agent: string;
72
- taskIndex?: number;
73
- output: string;
74
- exitCode: number | null;
75
- error?: string;
76
- model?: string;
77
- attemptedModels?: string[];
78
- outputTargetPath?: string;
79
- outputTargetExists?: boolean;
80
- }
81
-
82
- export function aggregateParallelOutputs(
83
- results: ParallelTaskResult[],
84
- headerFormat: (index: number, agent: string) => string = (i, agent) =>
85
- `=== Parallel Task ${i + 1} (${agent}) ===`,
86
- ): string {
87
- return results
88
- .map((r, i) => {
89
- const header = headerFormat(r.taskIndex ?? i, r.agent);
90
- const hasOutput = Boolean(r.output?.trim());
91
- const status =
92
- r.exitCode === -1
93
- ? "SKIPPED"
94
- : r.exitCode !== 0 && r.exitCode !== null
95
- ? `FAILED (exit code ${r.exitCode})${r.error ? `: ${r.error}` : ""}`
96
- : r.error
97
- ? `WARNING: ${r.error}`
98
- : !hasOutput && r.outputTargetPath && r.outputTargetExists === false
99
- ? `EMPTY OUTPUT (expected output file missing: ${r.outputTargetPath})`
100
- : !hasOutput && !r.outputTargetPath
101
- ? "EMPTY OUTPUT (no textual response returned)"
102
- : "";
103
- const body = status ? (hasOutput ? `${status}\n${r.output}` : status) : r.output;
104
- return `${header}\n${body}`;
105
- })
106
- .join("\n\n");
107
- }
108
-
109
- export const MAX_PARALLEL_CONCURRENCY = 4;