@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.
- package/README.md +253 -211
- package/extensions/ce-core/index.ts +190 -7
- package/extensions/ce-core/tools/parallel-subagent.ts +3 -3
- package/extensions/ce-core/tools/subagent.ts +2 -2
- package/package.json +4 -20
- package/rules/common/naming.md +154 -0
- package/skills/02-plan/SKILL.md +3 -2
- package/skills/03-work/SKILL.md +9 -6
- package/skills/04-review/SKILL.md +3 -2
- package/skills/06-next/SKILL.md +38 -10
- package/skills/06-next/references/recommendation-logic.md +2 -2
- package/skills/07-worktree/SKILL.md +9 -8
- package/skills/{09-help → 08-help}/SKILL.md +3 -3
- package/skills/{09-help → 08-help}/references/workflow-sequence.md +2 -1
- package/skills/references/language-detection.md +44 -0
- package/LICENSE +0 -21
- package/THIRD-PARTY-NOTICES.md +0 -35
- package/extensions/subagent/__tests__/async-job-tracker.test.ts +0 -21
- package/extensions/subagent/__tests__/execution-activity.test.ts +0 -15
- package/extensions/subagent/__tests__/parallel-render-stress.test.ts +0 -82
- package/extensions/subagent/__tests__/render-dedup.test.ts +0 -98
- package/extensions/subagent/__tests__/render-widget.test.ts +0 -28
- package/extensions/subagent/__tests__/throttle.test.ts +0 -90
- package/extensions/subagent/agent-management.ts +0 -596
- package/extensions/subagent/agent-manager-chain-detail.ts +0 -163
- package/extensions/subagent/agent-manager-detail.ts +0 -232
- package/extensions/subagent/agent-manager-edit.ts +0 -391
- package/extensions/subagent/agent-manager-list.ts +0 -279
- package/extensions/subagent/agent-manager-parallel.ts +0 -305
- package/extensions/subagent/agent-manager.ts +0 -706
- package/extensions/subagent/agent-scope.ts +0 -9
- package/extensions/subagent/agent-selection.ts +0 -26
- package/extensions/subagent/agent-serializer.ts +0 -124
- package/extensions/subagent/agent-templates.ts +0 -63
- package/extensions/subagent/agents/context-builder.md +0 -37
- package/extensions/subagent/agents/delegate.md +0 -9
- package/extensions/subagent/agents/oracle.md +0 -73
- package/extensions/subagent/agents/planner.md +0 -52
- package/extensions/subagent/agents/researcher.md +0 -50
- package/extensions/subagent/agents/reviewer.md +0 -38
- package/extensions/subagent/agents/scout.md +0 -48
- package/extensions/subagent/agents/worker.md +0 -52
- package/extensions/subagent/agents.ts +0 -762
- package/extensions/subagent/artifacts.ts +0 -101
- package/extensions/subagent/async-execution.ts +0 -521
- package/extensions/subagent/async-job-tracker.ts +0 -240
- package/extensions/subagent/async-status.ts +0 -242
- package/extensions/subagent/chain-clarify.ts +0 -1365
- package/extensions/subagent/chain-execution.ts +0 -854
- package/extensions/subagent/chain-serializer.ts +0 -127
- package/extensions/subagent/completion-dedupe.ts +0 -66
- package/extensions/subagent/doctor.ts +0 -201
- package/extensions/subagent/execution.ts +0 -748
- package/extensions/subagent/file-coalescer.ts +0 -43
- package/extensions/subagent/fork-context.ts +0 -64
- package/extensions/subagent/formatters.ts +0 -123
- package/extensions/subagent/frontmatter.ts +0 -32
- package/extensions/subagent/index.ts +0 -586
- package/extensions/subagent/intercom-bridge.ts +0 -241
- package/extensions/subagent/jsonl-writer.ts +0 -84
- package/extensions/subagent/model-fallback.ts +0 -109
- package/extensions/subagent/notify.ts +0 -111
- package/extensions/subagent/parallel-utils.ts +0 -109
- package/extensions/subagent/pi-args.ts +0 -139
- package/extensions/subagent/pi-spawn.ts +0 -101
- package/extensions/subagent/post-exit-stdio-guard.ts +0 -88
- package/extensions/subagent/prompt-template-bridge.ts +0 -400
- package/extensions/subagent/prompts/gather-context-and-clarify.md +0 -13
- package/extensions/subagent/prompts/parallel-cleanup.md +0 -42
- package/extensions/subagent/prompts/parallel-research.md +0 -50
- package/extensions/subagent/prompts/parallel-review.md +0 -40
- package/extensions/subagent/render-helpers.ts +0 -83
- package/extensions/subagent/render.ts +0 -908
- package/extensions/subagent/result-intercom.ts +0 -238
- package/extensions/subagent/result-watcher.ts +0 -172
- package/extensions/subagent/run-history.ts +0 -58
- package/extensions/subagent/run-status.ts +0 -137
- package/extensions/subagent/schemas.ts +0 -165
- package/extensions/subagent/session-tokens.ts +0 -51
- package/extensions/subagent/settings.ts +0 -368
- package/extensions/subagent/single-output.ts +0 -98
- package/extensions/subagent/skills.ts +0 -627
- package/extensions/subagent/slash-bridge.ts +0 -177
- package/extensions/subagent/slash-commands.ts +0 -304
- package/extensions/subagent/slash-live-state.ts +0 -295
- package/extensions/subagent/subagent-control.ts +0 -151
- package/extensions/subagent/subagent-executor.ts +0 -1915
- package/extensions/subagent/subagent-prompt-runtime.ts +0 -76
- package/extensions/subagent/subagent-runner.ts +0 -1471
- package/extensions/subagent/subagents-status.ts +0 -473
- package/extensions/subagent/text-editor.ts +0 -273
- package/extensions/subagent/throttle.ts +0 -77
- package/extensions/subagent/top-level-async.ts +0 -16
- package/extensions/subagent/types.ts +0 -624
- package/extensions/subagent/utils.ts +0 -457
- package/extensions/subagent/worktree.ts +0 -580
- package/extensions/super-pi-extension/agents/ce-oracle.md +0 -109
- package/extensions/super-pi-extension/agents/ce-planner.md +0 -116
- package/extensions/super-pi-extension/agents/ce-reviewer.md +0 -99
- package/extensions/super-pi-extension/agents/ce-scout.md +0 -80
- package/extensions/super-pi-extension/agents/ce-worker.md +0 -78
- package/extensions/super-pi-extension/chains/ce-parallel-review.chain.md +0 -34
- package/extensions/super-pi-extension/chains/ce-review-only.chain.md +0 -12
- package/extensions/super-pi-extension/chains/ce-standard.chain.md +0 -28
- package/extensions/super-pi-extension/index.ts +0 -129
- package/extensions/super-pi-extension/model-sync.ts +0 -204
- package/skills/08-status/SKILL.md +0 -41
- package/skills/08-status/references/artifact-locations.md +0 -10
- package/skills/10-rules/SKILL.md +0 -76
- 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;
|