@leing2021/super-pi 0.21.0 → 0.22.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 +29 -16
- package/extensions/subagent/agent-management.ts +595 -0
- package/extensions/subagent/agent-manager-chain-detail.ts +162 -0
- package/extensions/subagent/agent-manager-detail.ts +231 -0
- package/extensions/subagent/agent-manager-edit.ts +390 -0
- package/extensions/subagent/agent-manager-list.ts +278 -0
- package/extensions/subagent/agent-manager-parallel.ts +304 -0
- package/extensions/subagent/agent-manager.ts +705 -0
- package/extensions/subagent/agent-scope.ts +8 -0
- package/extensions/subagent/agent-selection.ts +25 -0
- package/extensions/subagent/agent-serializer.ts +123 -0
- package/extensions/subagent/agent-templates.ts +62 -0
- package/extensions/subagent/agents/context-builder.md +37 -0
- package/extensions/subagent/agents/delegate.md +9 -0
- package/extensions/subagent/agents/oracle.md +73 -0
- package/extensions/subagent/agents/planner.md +52 -0
- package/extensions/subagent/agents/researcher.md +50 -0
- package/extensions/subagent/agents/reviewer.md +38 -0
- package/extensions/subagent/agents/scout.md +48 -0
- package/extensions/subagent/agents/worker.md +52 -0
- package/extensions/subagent/agents.ts +761 -0
- package/extensions/subagent/artifacts.ts +100 -0
- package/extensions/subagent/async-execution.ts +520 -0
- package/extensions/subagent/async-job-tracker.ts +216 -0
- package/extensions/subagent/async-status.ts +241 -0
- package/extensions/subagent/chain-clarify.ts +1364 -0
- package/extensions/subagent/chain-execution.ts +853 -0
- package/extensions/subagent/chain-serializer.ts +126 -0
- package/extensions/subagent/completion-dedupe.ts +65 -0
- package/extensions/subagent/doctor.ts +200 -0
- package/extensions/subagent/execution.ts +738 -0
- package/extensions/subagent/file-coalescer.ts +42 -0
- package/extensions/subagent/fork-context.ts +63 -0
- package/extensions/subagent/formatters.ts +122 -0
- package/extensions/subagent/frontmatter.ts +31 -0
- package/extensions/subagent/index.ts +585 -0
- package/extensions/subagent/intercom-bridge.ts +240 -0
- package/extensions/subagent/jsonl-writer.ts +83 -0
- package/extensions/subagent/model-fallback.ts +108 -0
- package/extensions/subagent/notify.ts +110 -0
- package/extensions/subagent/parallel-utils.ts +108 -0
- package/extensions/subagent/pi-args.ts +138 -0
- package/extensions/subagent/pi-spawn.ts +100 -0
- package/extensions/subagent/post-exit-stdio-guard.ts +87 -0
- package/extensions/subagent/prompt-template-bridge.ts +399 -0
- package/extensions/subagent/prompts/gather-context-and-clarify.md +13 -0
- package/extensions/subagent/prompts/parallel-cleanup.md +42 -0
- package/extensions/subagent/prompts/parallel-research.md +50 -0
- package/extensions/subagent/prompts/parallel-review.md +40 -0
- package/extensions/subagent/render-helpers.ts +82 -0
- package/extensions/subagent/render.ts +836 -0
- package/extensions/subagent/result-intercom.ts +237 -0
- package/extensions/subagent/result-watcher.ts +171 -0
- package/extensions/subagent/run-history.ts +57 -0
- package/extensions/subagent/run-status.ts +136 -0
- package/extensions/subagent/schemas.ts +164 -0
- package/extensions/subagent/session-tokens.ts +50 -0
- package/extensions/subagent/settings.ts +367 -0
- package/extensions/subagent/single-output.ts +97 -0
- package/extensions/subagent/skills.ts +626 -0
- package/extensions/subagent/slash-bridge.ts +176 -0
- package/extensions/subagent/slash-commands.ts +303 -0
- package/extensions/subagent/slash-live-state.ts +294 -0
- package/extensions/subagent/subagent-control.ts +150 -0
- package/extensions/subagent/subagent-executor.ts +1899 -0
- package/extensions/subagent/subagent-prompt-runtime.ts +75 -0
- package/extensions/subagent/subagent-runner.ts +1470 -0
- package/extensions/subagent/subagents-status.ts +472 -0
- package/extensions/subagent/text-editor.ts +272 -0
- package/extensions/subagent/top-level-async.ts +15 -0
- package/extensions/subagent/types.ts +623 -0
- package/extensions/subagent/utils.ts +456 -0
- package/extensions/subagent/worktree.ts +579 -0
- package/extensions/super-pi-extension/index.ts +2 -55
- package/package.json +12 -6
- package/skills/pi-subagents/SKILL.md +566 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
// Based on pi-subagents by Nico Bailon (https://github.com/nicobailon/pi-subagents)
|
|
2
|
+
// MIT License
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as os from "node:os";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
|
|
8
|
+
export interface WorktreeSetup {
|
|
9
|
+
cwd: string;
|
|
10
|
+
worktrees: WorktreeInfo[];
|
|
11
|
+
baseCommit: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface WorktreeInfo {
|
|
15
|
+
path: string;
|
|
16
|
+
agentCwd: string;
|
|
17
|
+
branch: string;
|
|
18
|
+
index: number;
|
|
19
|
+
nodeModulesLinked: boolean;
|
|
20
|
+
syntheticPaths: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface WorktreeDiff {
|
|
24
|
+
index: number;
|
|
25
|
+
agent: string;
|
|
26
|
+
branch: string;
|
|
27
|
+
diffStat: string;
|
|
28
|
+
filesChanged: number;
|
|
29
|
+
insertions: number;
|
|
30
|
+
deletions: number;
|
|
31
|
+
patchPath: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface WorktreeTaskCwdConflict {
|
|
35
|
+
index: number;
|
|
36
|
+
agent: string;
|
|
37
|
+
cwd: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface WorktreeSetupHookConfig {
|
|
41
|
+
hookPath: string;
|
|
42
|
+
timeoutMs?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CreateWorktreesOptions {
|
|
46
|
+
agents?: string[];
|
|
47
|
+
setupHook?: WorktreeSetupHookConfig;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ResolvedWorktreeSetupHook {
|
|
51
|
+
hookPath: string;
|
|
52
|
+
timeoutMs: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface WorktreeSetupHookInput {
|
|
56
|
+
version: 1;
|
|
57
|
+
repoRoot: string;
|
|
58
|
+
worktreePath: string;
|
|
59
|
+
agentCwd: string;
|
|
60
|
+
branch: string;
|
|
61
|
+
index: number;
|
|
62
|
+
runId: string;
|
|
63
|
+
baseCommit: string;
|
|
64
|
+
agent?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface WorktreeSetupHookOutput {
|
|
68
|
+
syntheticPaths?: string[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface GitResult {
|
|
72
|
+
stdout: string;
|
|
73
|
+
stderr: string;
|
|
74
|
+
status: number | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface RepoState {
|
|
78
|
+
toplevel: string;
|
|
79
|
+
cwdRelative: string;
|
|
80
|
+
baseCommit: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const DEFAULT_WORKTREE_SETUP_HOOK_TIMEOUT_MS = 30000;
|
|
84
|
+
|
|
85
|
+
function runGit(cwd: string, args: string[]): GitResult {
|
|
86
|
+
const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8" });
|
|
87
|
+
return {
|
|
88
|
+
stdout: result.stdout ?? "",
|
|
89
|
+
stderr: result.stderr ?? "",
|
|
90
|
+
status: result.status,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function runGitChecked(cwd: string, args: string[]): string {
|
|
95
|
+
const result = runGit(cwd, args);
|
|
96
|
+
if (result.status !== 0) {
|
|
97
|
+
const command = `git -C ${cwd} ${args.join(" ")}`;
|
|
98
|
+
const message = result.stderr.trim() || result.stdout.trim() || `${command} failed`;
|
|
99
|
+
throw new Error(message);
|
|
100
|
+
}
|
|
101
|
+
return result.stdout;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolveRepoState(cwd: string): RepoState {
|
|
105
|
+
const cwdRelative = resolveRepoCwdRelative(cwd);
|
|
106
|
+
const toplevel = runGitChecked(cwd, ["rev-parse", "--show-toplevel"]).trim();
|
|
107
|
+
|
|
108
|
+
const status = runGitChecked(toplevel, ["status", "--porcelain"]);
|
|
109
|
+
if (status.trim().length > 0) {
|
|
110
|
+
throw new Error("worktree isolation requires a clean git working tree. Commit or stash changes first.");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const baseCommit = runGitChecked(toplevel, ["rev-parse", "HEAD"]).trim();
|
|
114
|
+
return { toplevel, cwdRelative, baseCommit };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function normalizeComparableCwd(cwd: string): string {
|
|
118
|
+
const resolved = path.resolve(cwd);
|
|
119
|
+
try {
|
|
120
|
+
return fs.realpathSync(resolved);
|
|
121
|
+
} catch {
|
|
122
|
+
// Use the unresolved absolute path when realpath resolution is unavailable.
|
|
123
|
+
return resolved;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function findWorktreeTaskCwdConflict(
|
|
128
|
+
tasks: ReadonlyArray<{ agent: string; cwd?: string }>,
|
|
129
|
+
sharedCwd: string,
|
|
130
|
+
): WorktreeTaskCwdConflict | undefined {
|
|
131
|
+
const normalizedSharedCwd = normalizeComparableCwd(sharedCwd);
|
|
132
|
+
for (let index = 0; index < tasks.length; index++) {
|
|
133
|
+
const task = tasks[index]!;
|
|
134
|
+
if (!task.cwd) continue;
|
|
135
|
+
const taskCwd = path.isAbsolute(task.cwd) ? task.cwd : path.resolve(sharedCwd, task.cwd);
|
|
136
|
+
if (normalizeComparableCwd(taskCwd) === normalizedSharedCwd) continue;
|
|
137
|
+
return { index, agent: task.agent, cwd: task.cwd };
|
|
138
|
+
}
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function formatWorktreeTaskCwdConflict(
|
|
143
|
+
conflict: WorktreeTaskCwdConflict,
|
|
144
|
+
sharedCwd: string,
|
|
145
|
+
): string {
|
|
146
|
+
return `worktree isolation uses the shared cwd (${sharedCwd}); task ${conflict.index + 1} (${conflict.agent}) sets cwd to ${conflict.cwd}. Remove task-level cwd overrides or disable worktree.`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function safePatchAgentName(agent: string): string {
|
|
150
|
+
return agent.replace(/[^\w.-]/g, "_");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildWorktreeBranch(runId: string, index: number): string {
|
|
154
|
+
return `pi-parallel-${runId}-${index}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildWorktreePath(runId: string, index: number): string {
|
|
158
|
+
return path.join(os.tmpdir(), `pi-worktree-${runId}-${index}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function resolveRepoCwdRelative(cwd: string): string {
|
|
162
|
+
const repoCheck = runGit(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
163
|
+
if (repoCheck.status !== 0 || repoCheck.stdout.trim() !== "true") {
|
|
164
|
+
throw new Error("worktree isolation requires a git repository");
|
|
165
|
+
}
|
|
166
|
+
const rawPrefix = runGitChecked(cwd, ["rev-parse", "--show-prefix"]).trim();
|
|
167
|
+
const normalizedPrefix = rawPrefix
|
|
168
|
+
? path.normalize(rawPrefix.replace(/[\\/]+$/, ""))
|
|
169
|
+
: "";
|
|
170
|
+
return normalizedPrefix === "." ? "" : normalizedPrefix;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function resolveExpectedWorktreeAgentCwd(cwd: string, runId: string, index: number): string {
|
|
174
|
+
const cwdRelative = resolveRepoCwdRelative(cwd);
|
|
175
|
+
const worktreePath = buildWorktreePath(runId, index);
|
|
176
|
+
return cwdRelative ? path.join(worktreePath, cwdRelative) : worktreePath;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function linkNodeModulesIfPresent(toplevel: string, worktreePath: string): boolean {
|
|
180
|
+
const nodeModulesPath = path.join(toplevel, "node_modules");
|
|
181
|
+
const nodeModulesLinkPath = path.join(worktreePath, "node_modules");
|
|
182
|
+
if (!fs.existsSync(nodeModulesPath) || fs.existsSync(nodeModulesLinkPath)) return false;
|
|
183
|
+
try {
|
|
184
|
+
fs.symlinkSync(nodeModulesPath, nodeModulesLinkPath);
|
|
185
|
+
return true;
|
|
186
|
+
} catch {
|
|
187
|
+
// Symlink creation is optional (e.g., unsupported filesystems on CI runners).
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function parseHookTimeout(timeoutMs: number | undefined): number {
|
|
193
|
+
if (timeoutMs === undefined) return DEFAULT_WORKTREE_SETUP_HOOK_TIMEOUT_MS;
|
|
194
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
|
|
195
|
+
throw new Error("worktree setup hook timeout must be an integer greater than 0");
|
|
196
|
+
}
|
|
197
|
+
return timeoutMs;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function resolveWorktreeSetupHook(
|
|
201
|
+
repoRoot: string,
|
|
202
|
+
config: WorktreeSetupHookConfig | undefined,
|
|
203
|
+
): ResolvedWorktreeSetupHook | undefined {
|
|
204
|
+
if (!config) return undefined;
|
|
205
|
+
const hookPath = config.hookPath.trim();
|
|
206
|
+
if (!hookPath) {
|
|
207
|
+
throw new Error("worktree setup hook path cannot be empty");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const expandedHookPath = hookPath.startsWith("~/") ? path.join(os.homedir(), hookPath.slice(2)) : hookPath;
|
|
211
|
+
let resolvedPath: string;
|
|
212
|
+
if (path.isAbsolute(expandedHookPath)) {
|
|
213
|
+
resolvedPath = expandedHookPath;
|
|
214
|
+
} else if (expandedHookPath.includes("/") || expandedHookPath.includes("\\")) {
|
|
215
|
+
resolvedPath = path.resolve(repoRoot, expandedHookPath);
|
|
216
|
+
} else {
|
|
217
|
+
throw new Error("worktree setup hook must be an absolute path or a repo-relative path");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
221
|
+
throw new Error(`worktree setup hook not found: ${resolvedPath}`);
|
|
222
|
+
}
|
|
223
|
+
if (fs.statSync(resolvedPath).isDirectory()) {
|
|
224
|
+
throw new Error(`worktree setup hook must be a file, got directory: ${resolvedPath}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
hookPath: resolvedPath,
|
|
229
|
+
timeoutMs: parseHookTimeout(config.timeoutMs),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function normalizeSyntheticPath(worktreePath: string, rawPath: string): string {
|
|
234
|
+
const trimmed = rawPath.trim();
|
|
235
|
+
if (!trimmed) throw new Error("synthetic path cannot be empty");
|
|
236
|
+
if (path.isAbsolute(trimmed)) throw new Error(`synthetic path must be relative: ${rawPath}`);
|
|
237
|
+
|
|
238
|
+
const resolved = path.resolve(worktreePath, trimmed);
|
|
239
|
+
const relative = path.relative(worktreePath, resolved);
|
|
240
|
+
if (!relative || relative === ".") {
|
|
241
|
+
throw new Error(`synthetic path cannot target the worktree root: ${rawPath}`);
|
|
242
|
+
}
|
|
243
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
244
|
+
throw new Error(`synthetic path escapes the worktree root: ${rawPath}`);
|
|
245
|
+
}
|
|
246
|
+
return path.normalize(relative);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function hasTrackedEntries(worktreePath: string, relativePath: string): boolean {
|
|
250
|
+
const result = runGit(worktreePath, ["ls-files", "--", relativePath]);
|
|
251
|
+
return result.status === 0 && result.stdout.trim().length > 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function parseWorktreeSetupHookOutput(rawStdout: string): WorktreeSetupHookOutput {
|
|
255
|
+
const trimmed = rawStdout.trim();
|
|
256
|
+
if (!trimmed) {
|
|
257
|
+
throw new Error("worktree setup hook returned empty stdout; expected JSON object");
|
|
258
|
+
}
|
|
259
|
+
let parsed: unknown;
|
|
260
|
+
try {
|
|
261
|
+
parsed = JSON.parse(trimmed);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
264
|
+
throw new Error(`worktree setup hook returned invalid JSON: ${message}`);
|
|
265
|
+
}
|
|
266
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
267
|
+
throw new Error("worktree setup hook stdout must be a JSON object");
|
|
268
|
+
}
|
|
269
|
+
return parsed as WorktreeSetupHookOutput;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function runWorktreeSetupHook(
|
|
273
|
+
hook: ResolvedWorktreeSetupHook,
|
|
274
|
+
input: WorktreeSetupHookInput,
|
|
275
|
+
): string[] {
|
|
276
|
+
const result = spawnSync(hook.hookPath, [], {
|
|
277
|
+
cwd: input.worktreePath,
|
|
278
|
+
encoding: "utf-8",
|
|
279
|
+
input: JSON.stringify(input),
|
|
280
|
+
timeout: hook.timeoutMs,
|
|
281
|
+
shell: false,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
if (result.error) {
|
|
285
|
+
const code = "code" in result.error ? result.error.code : undefined;
|
|
286
|
+
if (code === "ETIMEDOUT") {
|
|
287
|
+
throw new Error(`worktree setup hook timed out after ${hook.timeoutMs}ms`);
|
|
288
|
+
}
|
|
289
|
+
throw new Error(`worktree setup hook failed: ${result.error.message}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (result.status !== 0) {
|
|
293
|
+
const details = result.stderr.trim() || result.stdout.trim() || "no output";
|
|
294
|
+
throw new Error(`worktree setup hook failed with exit code ${result.status}: ${details}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const output = parseWorktreeSetupHookOutput(result.stdout);
|
|
298
|
+
if (output.syntheticPaths === undefined) return [];
|
|
299
|
+
if (!Array.isArray(output.syntheticPaths)) {
|
|
300
|
+
throw new Error("worktree setup hook output field 'syntheticPaths' must be an array of relative paths");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const uniquePaths = new Set<string>();
|
|
304
|
+
for (const candidate of output.syntheticPaths) {
|
|
305
|
+
if (typeof candidate !== "string") {
|
|
306
|
+
throw new Error("worktree setup hook output field 'syntheticPaths' must contain only strings");
|
|
307
|
+
}
|
|
308
|
+
const normalizedPath = normalizeSyntheticPath(input.worktreePath, candidate);
|
|
309
|
+
if (hasTrackedEntries(input.worktreePath, normalizedPath)) {
|
|
310
|
+
throw new Error(`worktree setup hook cannot mark tracked paths as synthetic: ${normalizedPath}`);
|
|
311
|
+
}
|
|
312
|
+
uniquePaths.add(normalizedPath);
|
|
313
|
+
}
|
|
314
|
+
return [...uniquePaths];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function createSingleWorktree(
|
|
318
|
+
toplevel: string,
|
|
319
|
+
cwdRelative: string,
|
|
320
|
+
runId: string,
|
|
321
|
+
index: number,
|
|
322
|
+
baseCommit: string,
|
|
323
|
+
setupHook: ResolvedWorktreeSetupHook | undefined,
|
|
324
|
+
agent: string | undefined,
|
|
325
|
+
): WorktreeInfo {
|
|
326
|
+
const branch = buildWorktreeBranch(runId, index);
|
|
327
|
+
const worktreePath = buildWorktreePath(runId, index);
|
|
328
|
+
const add = runGit(toplevel, ["worktree", "add", worktreePath, "-b", branch, "HEAD"]);
|
|
329
|
+
if (add.status !== 0) {
|
|
330
|
+
const message = add.stderr.trim() || add.stdout.trim() || `failed to create worktree ${worktreePath}`;
|
|
331
|
+
throw new Error(message);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const agentCwd = cwdRelative ? path.join(worktreePath, cwdRelative) : worktreePath;
|
|
335
|
+
try {
|
|
336
|
+
const nodeModulesLinked = linkNodeModulesIfPresent(toplevel, worktreePath);
|
|
337
|
+
const syntheticPaths = nodeModulesLinked ? ["node_modules"] : [];
|
|
338
|
+
|
|
339
|
+
if (setupHook) {
|
|
340
|
+
const hookSyntheticPaths = runWorktreeSetupHook(setupHook, {
|
|
341
|
+
version: 1,
|
|
342
|
+
repoRoot: toplevel,
|
|
343
|
+
worktreePath,
|
|
344
|
+
agentCwd,
|
|
345
|
+
branch,
|
|
346
|
+
index,
|
|
347
|
+
runId,
|
|
348
|
+
baseCommit,
|
|
349
|
+
agent,
|
|
350
|
+
});
|
|
351
|
+
syntheticPaths.push(...hookSyntheticPaths);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
path: worktreePath,
|
|
356
|
+
agentCwd,
|
|
357
|
+
branch,
|
|
358
|
+
index,
|
|
359
|
+
nodeModulesLinked,
|
|
360
|
+
syntheticPaths,
|
|
361
|
+
};
|
|
362
|
+
} catch (error) {
|
|
363
|
+
try { runGitChecked(toplevel, ["worktree", "remove", "--force", worktreePath]); } catch {
|
|
364
|
+
// Best-effort rollback; preserve the original setup failure.
|
|
365
|
+
}
|
|
366
|
+
try { runGitChecked(toplevel, ["branch", "-D", branch]); } catch {
|
|
367
|
+
// Best-effort rollback; preserve the original setup failure.
|
|
368
|
+
}
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function removeSyntheticPath(worktree: WorktreeInfo, syntheticPath: string): void {
|
|
374
|
+
const resolved = path.resolve(worktree.path, syntheticPath);
|
|
375
|
+
const relative = path.relative(worktree.path, resolved);
|
|
376
|
+
if (!relative || relative === "." || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let stat: fs.Stats;
|
|
381
|
+
try {
|
|
382
|
+
stat = fs.lstatSync(resolved);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
const code = error && typeof error === "object" && "code" in error ? (error as { code?: unknown }).code : undefined;
|
|
385
|
+
if (code === "ENOENT") return;
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (stat.isSymbolicLink()) {
|
|
390
|
+
fs.unlinkSync(resolved);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (stat.isDirectory()) {
|
|
394
|
+
fs.rmSync(resolved, { recursive: true, force: true });
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
fs.rmSync(resolved, { force: true });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function removeSyntheticPathsBeforeDiff(worktree: WorktreeInfo): void {
|
|
401
|
+
if (worktree.syntheticPaths.length === 0) return;
|
|
402
|
+
const seen = new Set<string>();
|
|
403
|
+
for (const syntheticPath of worktree.syntheticPaths) {
|
|
404
|
+
if (seen.has(syntheticPath)) continue;
|
|
405
|
+
seen.add(syntheticPath);
|
|
406
|
+
removeSyntheticPath(worktree, syntheticPath);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function emptyDiff(index: number, agent: string, branch: string, patchPath: string): WorktreeDiff {
|
|
411
|
+
return {
|
|
412
|
+
index,
|
|
413
|
+
agent,
|
|
414
|
+
branch,
|
|
415
|
+
diffStat: "",
|
|
416
|
+
filesChanged: 0,
|
|
417
|
+
insertions: 0,
|
|
418
|
+
deletions: 0,
|
|
419
|
+
patchPath,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function parseNumstat(numstat: string): { filesChanged: number; insertions: number; deletions: number } {
|
|
424
|
+
const lines = numstat
|
|
425
|
+
.split("\n")
|
|
426
|
+
.map((line) => line.trim())
|
|
427
|
+
.filter(Boolean);
|
|
428
|
+
let filesChanged = 0;
|
|
429
|
+
let insertions = 0;
|
|
430
|
+
let deletions = 0;
|
|
431
|
+
|
|
432
|
+
for (const line of lines) {
|
|
433
|
+
const [rawInsertions, rawDeletions] = line.split("\t");
|
|
434
|
+
if (rawInsertions === undefined || rawDeletions === undefined) continue;
|
|
435
|
+
filesChanged++;
|
|
436
|
+
if (/^\d+$/.test(rawInsertions)) insertions += parseInt(rawInsertions, 10);
|
|
437
|
+
if (/^\d+$/.test(rawDeletions)) deletions += parseInt(rawDeletions, 10);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return { filesChanged, insertions, deletions };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function captureWorktreeDiff(
|
|
444
|
+
setup: WorktreeSetup,
|
|
445
|
+
worktree: WorktreeInfo,
|
|
446
|
+
agent: string,
|
|
447
|
+
patchPath: string,
|
|
448
|
+
): WorktreeDiff {
|
|
449
|
+
removeSyntheticPathsBeforeDiff(worktree);
|
|
450
|
+
runGitChecked(worktree.path, ["add", "-A"]);
|
|
451
|
+
const diffStat = runGitChecked(worktree.path, ["diff", "--cached", "--stat", setup.baseCommit]).trim();
|
|
452
|
+
const patch = runGitChecked(worktree.path, ["diff", "--cached", setup.baseCommit]);
|
|
453
|
+
const numstat = runGitChecked(worktree.path, ["diff", "--cached", "--numstat", setup.baseCommit]);
|
|
454
|
+
fs.writeFileSync(patchPath, patch, "utf-8");
|
|
455
|
+
|
|
456
|
+
if (!patch.trim()) {
|
|
457
|
+
return emptyDiff(worktree.index, agent, worktree.branch, patchPath);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const parsed = parseNumstat(numstat);
|
|
461
|
+
return {
|
|
462
|
+
index: worktree.index,
|
|
463
|
+
agent,
|
|
464
|
+
branch: worktree.branch,
|
|
465
|
+
diffStat,
|
|
466
|
+
filesChanged: parsed.filesChanged,
|
|
467
|
+
insertions: parsed.insertions,
|
|
468
|
+
deletions: parsed.deletions,
|
|
469
|
+
patchPath,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function writeEmptyPatch(patchPath: string): void {
|
|
474
|
+
try {
|
|
475
|
+
fs.writeFileSync(patchPath, "", "utf-8");
|
|
476
|
+
} catch {
|
|
477
|
+
// Diff artifact writing is best-effort in error paths.
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function cleanupSingleWorktree(repoCwd: string, worktree: WorktreeInfo): void {
|
|
482
|
+
try { runGitChecked(repoCwd, ["worktree", "remove", "--force", worktree.path]); } catch {
|
|
483
|
+
// Cleanup is best-effort to avoid masking caller errors.
|
|
484
|
+
}
|
|
485
|
+
try { runGitChecked(repoCwd, ["branch", "-D", worktree.branch]); } catch {
|
|
486
|
+
// Cleanup is best-effort to avoid masking caller errors.
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function hasWorktreeChanges(diff: WorktreeDiff): boolean {
|
|
491
|
+
return diff.filesChanged > 0 || diff.insertions > 0 || diff.deletions > 0 || diff.diffStat.trim().length > 0;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export function createWorktrees(cwd: string, runId: string, count: number, options?: CreateWorktreesOptions): WorktreeSetup {
|
|
495
|
+
const repo = resolveRepoState(cwd);
|
|
496
|
+
const setupHook = resolveWorktreeSetupHook(repo.toplevel, options?.setupHook);
|
|
497
|
+
const worktrees: WorktreeInfo[] = [];
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
for (let index = 0; index < count; index++) {
|
|
501
|
+
worktrees.push(createSingleWorktree(
|
|
502
|
+
repo.toplevel,
|
|
503
|
+
repo.cwdRelative,
|
|
504
|
+
runId,
|
|
505
|
+
index,
|
|
506
|
+
repo.baseCommit,
|
|
507
|
+
setupHook,
|
|
508
|
+
options?.agents?.[index],
|
|
509
|
+
));
|
|
510
|
+
}
|
|
511
|
+
} catch (error) {
|
|
512
|
+
cleanupWorktrees({
|
|
513
|
+
cwd: repo.toplevel,
|
|
514
|
+
worktrees,
|
|
515
|
+
baseCommit: repo.baseCommit,
|
|
516
|
+
});
|
|
517
|
+
throw error;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
cwd: repo.toplevel,
|
|
522
|
+
worktrees,
|
|
523
|
+
baseCommit: repo.baseCommit,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export function diffWorktrees(setup: WorktreeSetup, agents: string[], diffsDir: string): WorktreeDiff[] {
|
|
528
|
+
try {
|
|
529
|
+
fs.mkdirSync(diffsDir, { recursive: true });
|
|
530
|
+
} catch {
|
|
531
|
+
// Returning no diffs is safer than failing the whole command on artifact-dir issues.
|
|
532
|
+
return [];
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const diffs: WorktreeDiff[] = [];
|
|
536
|
+
for (let index = 0; index < setup.worktrees.length; index++) {
|
|
537
|
+
const worktree = setup.worktrees[index]!;
|
|
538
|
+
const agent = agents[index] ?? `task-${index + 1}`;
|
|
539
|
+
const patchPath = path.join(diffsDir, `task-${index}-${safePatchAgentName(agent)}.patch`);
|
|
540
|
+
try {
|
|
541
|
+
diffs.push(captureWorktreeDiff(setup, worktree, agent, patchPath));
|
|
542
|
+
} catch {
|
|
543
|
+
// Preserve execution flow; failed diff capture maps to an empty per-task patch.
|
|
544
|
+
writeEmptyPatch(patchPath);
|
|
545
|
+
diffs.push(emptyDiff(index, agent, worktree.branch, patchPath));
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return diffs;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export function cleanupWorktrees(setup: WorktreeSetup): void {
|
|
553
|
+
for (let index = setup.worktrees.length - 1; index >= 0; index--) {
|
|
554
|
+
cleanupSingleWorktree(setup.cwd, setup.worktrees[index]!);
|
|
555
|
+
}
|
|
556
|
+
try { runGitChecked(setup.cwd, ["worktree", "prune"]); } catch {
|
|
557
|
+
// Pruning is best-effort cleanup.
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export function formatWorktreeDiffSummary(diffs: WorktreeDiff[]): string {
|
|
562
|
+
const changed = diffs.filter(hasWorktreeChanges);
|
|
563
|
+
if (changed.length === 0) return "";
|
|
564
|
+
|
|
565
|
+
const lines: string[] = ["=== Worktree Changes ===", ""];
|
|
566
|
+
for (const diff of changed) {
|
|
567
|
+
lines.push(
|
|
568
|
+
`--- Task ${diff.index + 1} (${diff.agent}): ${diff.filesChanged} files changed, +${diff.insertions} -${diff.deletions} ---`,
|
|
569
|
+
);
|
|
570
|
+
if (diff.diffStat.trim().length > 0) {
|
|
571
|
+
lines.push(diff.diffStat);
|
|
572
|
+
}
|
|
573
|
+
lines.push("");
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const patchesDir = path.dirname(changed[0]!.patchPath);
|
|
577
|
+
lines.push(`Full patches: ${patchesDir}`);
|
|
578
|
+
return lines.join("\n").trimEnd();
|
|
579
|
+
}
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* super-pi-extension
|
|
3
3
|
*
|
|
4
|
-
* Compound Engineering
|
|
4
|
+
* Compound Engineering extension
|
|
5
5
|
*
|
|
6
6
|
* Features:
|
|
7
7
|
* - Pre-configured CE Agents (ce-scout, ce-planner, ce-worker, ce-reviewer, ce-oracle)
|
|
8
8
|
* - Pre-configured CE Chains (ce-standard, ce-review-only, ce-parallel-review)
|
|
9
9
|
* - Model strategy sync: modelStrategy[stage] → subagents.agentOverrides[agent].model
|
|
10
10
|
* - Thinking strategy sync: thinkingStrategy[stage] → subagents.agentOverrides[agent].thinking
|
|
11
|
-
* - Graceful dependency detection for pi-subagents
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
13
|
import * as fs from "node:fs";
|
|
15
14
|
import * as path from "node:path";
|
|
16
15
|
import * as os from "node:os";
|
|
17
|
-
import { execSync } from "node:child_process";
|
|
18
16
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
19
17
|
|
|
20
18
|
// CE Stage to Agent mapping
|
|
@@ -119,49 +117,6 @@ function autoSyncSettings(projectRoot?: string): void {
|
|
|
119
117
|
}
|
|
120
118
|
}
|
|
121
119
|
|
|
122
|
-
/**
|
|
123
|
-
* Check if pi-subagents extension is installed
|
|
124
|
-
*/
|
|
125
|
-
function isPiSubagentsInstalled(): boolean {
|
|
126
|
-
const extensionsDir = path.join(os.homedir(), ".pi", "agent", "extensions", "subagent");
|
|
127
|
-
return fs.existsSync(extensionsDir) && fs.existsSync(path.join(extensionsDir, "index.ts"));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Try to install pi-subagents automatically
|
|
132
|
-
*/
|
|
133
|
-
function tryAutoInstallPiSubagents(): boolean {
|
|
134
|
-
try {
|
|
135
|
-
console.log("[super-pi-extension] Attempting to install pi-subagents...");
|
|
136
|
-
execSync("pi install npm:pi-subagents", { stdio: "inherit" });
|
|
137
|
-
return true;
|
|
138
|
-
} catch (error) {
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Format installation instructions
|
|
145
|
-
*/
|
|
146
|
-
function formatInstallInstructions(): string {
|
|
147
|
-
return `
|
|
148
|
-
╔══════════════════════════════════════════════════════════════════╗
|
|
149
|
-
║ super-pi Extension Loaded ║
|
|
150
|
-
╠══════════════════════════════════════════════════════════════════╣
|
|
151
|
-
║ ⚡ For enhanced CE workflow capabilities, install pi-subagents: ║
|
|
152
|
-
║ ║
|
|
153
|
-
║ pi install npm:pi-subagents ║
|
|
154
|
-
║ ║
|
|
155
|
-
║ This enables: ║
|
|
156
|
-
║ • /run ce-worker "execute plan" ║
|
|
157
|
-
║ • /run-chain ce-standard -- implement feature ║
|
|
158
|
-
║ • Parallel review with /run-chain ce-parallel-review -- ║
|
|
159
|
-
║ ║
|
|
160
|
-
║ Without it, CE Agents/Chains won't execute. ║
|
|
161
|
-
╚══════════════════════════════════════════════════════════════════╝
|
|
162
|
-
`;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
120
|
export default function (pi: ExtensionAPI) {
|
|
166
121
|
pi.on("session_start", async (_event, ctx) => {
|
|
167
122
|
const cwd = ctx.cwd || process.cwd();
|
|
@@ -169,14 +124,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
169
124
|
// Auto-sync modelStrategy + thinkingStrategy to agentOverrides
|
|
170
125
|
autoSyncSettings(cwd);
|
|
171
126
|
|
|
172
|
-
|
|
173
|
-
if (!isPiSubagentsInstalled()) {
|
|
174
|
-
console.warn("[super-pi-extension] pi-subagents not found. CE Agents/Chains require it.");
|
|
175
|
-
console.warn("[super-pi-extension] Run: pi install npm:pi-subagents");
|
|
176
|
-
} else {
|
|
177
|
-
console.log("[super-pi-extension] pi-subagents detected. Full CE workflow enabled.");
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
console.log("[super-pi-extension] Loaded. CE agents and chains available.");
|
|
127
|
+
console.log("[super-pi-extension] Loaded. CE agents, chains, and subagent tools available.");
|
|
181
128
|
});
|
|
182
129
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leing2021/super-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Pi-native Compound Engineering package for iterative development workflows",
|
|
@@ -35,9 +35,10 @@
|
|
|
35
35
|
"test": "bun test"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@mariozechner/pi-coding-agent": "*"
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
"@mariozechner/pi-coding-agent": "*"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"typebox": "^1.1.24"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"@mariozechner/pi-coding-agent": "0.69.0",
|
|
@@ -49,10 +50,15 @@
|
|
|
49
50
|
],
|
|
50
51
|
"extensions": [
|
|
51
52
|
"./extensions",
|
|
52
|
-
"./extensions/super-pi-extension"
|
|
53
|
+
"./extensions/super-pi-extension",
|
|
54
|
+
"./extensions/subagent"
|
|
53
55
|
],
|
|
54
56
|
"agents": [
|
|
55
|
-
"./extensions/super-pi-extension/agents"
|
|
57
|
+
"./extensions/super-pi-extension/agents",
|
|
58
|
+
"./extensions/subagent/agents"
|
|
59
|
+
],
|
|
60
|
+
"prompts": [
|
|
61
|
+
"./extensions/subagent/prompts"
|
|
56
62
|
],
|
|
57
63
|
"chains": [
|
|
58
64
|
"./extensions/super-pi-extension/chains"
|