@oh-my-pi/pi-coding-agent 6.8.5 → 6.9.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.
@@ -1,435 +0,0 @@
1
- import * as path from "node:path";
2
- import { nanoid } from "nanoid";
3
- import { type CollapseStrategy, collapse } from "../../../../lib/worktree/collapse";
4
- import { WorktreeError, WorktreeErrorCode } from "../../../../lib/worktree/errors";
5
- import { getRepoRoot, git } from "../../../../lib/worktree/git";
6
- import * as worktree from "../../../../lib/worktree/index";
7
- import { createSession, updateSession } from "../../../../lib/worktree/session";
8
- import { formatStats, getStats } from "../../../../lib/worktree/stats";
9
- import type { HookCommandContext } from "../../../hooks/types";
10
- import { discoverAgents, getAgent } from "../../../tools/task/discovery";
11
- import { runSubprocess } from "../../../tools/task/executor";
12
- import { generateTaskName } from "../../../tools/task/name-generator";
13
- import type { AgentDefinition } from "../../../tools/task/types";
14
- import type { CustomCommand, CustomCommandAPI } from "../../types";
15
-
16
- interface FlagParseResult {
17
- positionals: string[];
18
- flags: Map<string, string | boolean>;
19
- }
20
-
21
- interface NewArgs {
22
- branch: string;
23
- base?: string;
24
- }
25
-
26
- interface MergeArgs {
27
- source: string;
28
- target?: string;
29
- strategy?: CollapseStrategy;
30
- keep?: boolean;
31
- }
32
-
33
- interface RmArgs {
34
- name: string;
35
- force?: boolean;
36
- }
37
-
38
- interface SpawnArgs {
39
- task: string;
40
- scope?: string;
41
- name?: string;
42
- }
43
-
44
- interface ParallelTask {
45
- task: string;
46
- scope: string;
47
- }
48
-
49
- function parseFlags(args: string[]): FlagParseResult {
50
- const flags = new Map<string, string | boolean>();
51
- const positionals: string[] = [];
52
-
53
- for (let i = 0; i < args.length; i++) {
54
- const arg = args[i];
55
- if (arg.startsWith("--")) {
56
- const name = arg.slice(2);
57
- const next = args[i + 1];
58
- if (next && !next.startsWith("--")) {
59
- flags.set(name, next);
60
- i += 1;
61
- } else {
62
- flags.set(name, true);
63
- }
64
- } else {
65
- positionals.push(arg);
66
- }
67
- }
68
-
69
- return { positionals, flags };
70
- }
71
-
72
- function getFlagValue(flags: Map<string, string | boolean>, name: string): string | undefined {
73
- const value = flags.get(name);
74
- if (typeof value === "string") return value;
75
- return undefined;
76
- }
77
-
78
- function getFlagBoolean(flags: Map<string, string | boolean>, name: string): boolean {
79
- return flags.get(name) === true;
80
- }
81
-
82
- function formatUsage(): string {
83
- return [
84
- "Usage:",
85
- " /wt new <branch> [--base <ref>]",
86
- " /wt list",
87
- " /wt merge <src> [dst] [--strategy simple|merge-base|rebase] [--keep]",
88
- " /wt rm <name> [--force]",
89
- " /wt status",
90
- ' /wt spawn "<task>" [--scope <glob>] [--name <branch>]',
91
- " /wt parallel --task <t> --scope <s> [--task <t> --scope <s>]...",
92
- ].join("\n");
93
- }
94
-
95
- function formatError(err: unknown): string {
96
- if (err instanceof WorktreeError) {
97
- return `${err.code}: ${err.message}`;
98
- }
99
- if (err instanceof Error) return err.message;
100
- return String(err);
101
- }
102
-
103
- async function pickAgent(cwd: string): Promise<AgentDefinition> {
104
- const { agents } = await discoverAgents(cwd);
105
- // Use the bundled "task" agent as the general-purpose default.
106
- const agent = getAgent(agents, "task") ?? agents[0];
107
- if (!agent) {
108
- throw new Error("No agents available");
109
- }
110
- return agent;
111
- }
112
-
113
- function parseParallelTasks(args: string[]): ParallelTask[] {
114
- const tasks: ParallelTask[] = [];
115
- let current: Partial<ParallelTask> = {};
116
-
117
- for (let i = 0; i < args.length; i++) {
118
- const arg = args[i];
119
- if (arg === "--task") {
120
- const value = args[i + 1];
121
- if (!value || value.startsWith("--")) {
122
- throw new Error("Missing value for --task");
123
- }
124
- current.task = value;
125
- i += 1;
126
- } else if (arg === "--scope") {
127
- const value = args[i + 1];
128
- if (!value || value.startsWith("--")) {
129
- throw new Error("Missing value for --scope");
130
- }
131
- current.scope = value;
132
- i += 1;
133
- } else {
134
- throw new Error(`Unknown argument: ${arg}`);
135
- }
136
-
137
- if (current.task && current.scope) {
138
- tasks.push({ task: current.task, scope: current.scope });
139
- current = {};
140
- }
141
- }
142
-
143
- if (current.task || current.scope) {
144
- throw new Error("Each --task must be paired with a --scope");
145
- }
146
-
147
- return tasks;
148
- }
149
-
150
- function validateDisjointScopes(scopes: string[]): void {
151
- for (let i = 0; i < scopes.length; i++) {
152
- for (let j = i + 1; j < scopes.length; j++) {
153
- const a = scopes[i].replace(/\*.*$/, "");
154
- const b = scopes[j].replace(/\*.*$/, "");
155
- if (a.startsWith(b) || b.startsWith(a)) {
156
- throw new WorktreeError(
157
- `Overlapping scopes: "${scopes[i]}" and "${scopes[j]}"`,
158
- WorktreeErrorCode.OVERLAPPING_SCOPES,
159
- );
160
- }
161
- }
162
- }
163
- }
164
-
165
- async function handleNew(args: NewArgs): Promise<string> {
166
- const wt = await worktree.create(args.branch, { base: args.base });
167
-
168
- return [`Created worktree: ${wt.path}`, `Branch: ${wt.branch ?? "detached"}`, "", `To switch: cd ${wt.path}`].join(
169
- "\n",
170
- );
171
- }
172
-
173
- async function handleList(ctx: HookCommandContext): Promise<string> {
174
- const worktrees = await worktree.list();
175
- const cwd = path.resolve(ctx.cwd);
176
- const mainPath = await getRepoRoot();
177
-
178
- const lines: string[] = [];
179
-
180
- for (const wt of worktrees) {
181
- const stats = await getStats(wt.path);
182
- const isCurrent = cwd === wt.path || cwd.startsWith(wt.path + path.sep);
183
- const isMain = wt.path === mainPath;
184
-
185
- const marker = isCurrent ? "->" : " ";
186
- const mainTag = isMain ? " [main]" : "";
187
- const branch = wt.branch ?? "detached";
188
- const statsStr = formatStats(stats);
189
-
190
- lines.push(`${marker} ${branch}${mainTag} (${statsStr})`);
191
- }
192
-
193
- return lines.join("\n") || "No worktrees found";
194
- }
195
-
196
- async function handleMerge(args: MergeArgs): Promise<string> {
197
- const target = args.target ?? "main";
198
- const strategy = args.strategy ?? "rebase";
199
-
200
- const result = await collapse(args.source, target, {
201
- strategy,
202
- keepSource: args.keep,
203
- });
204
-
205
- const lines = [
206
- `Collapsed ${args.source} -> ${target}`,
207
- `Strategy: ${strategy}`,
208
- `Changes: +${result.insertions} -${result.deletions} in ${result.filesChanged} files`,
209
- ];
210
-
211
- if (!args.keep) {
212
- lines.push("Source worktree removed");
213
- }
214
-
215
- return lines.join("\n");
216
- }
217
-
218
- async function handleRm(args: RmArgs): Promise<string> {
219
- const wt = await worktree.find(args.name);
220
- await worktree.remove(args.name, { force: args.force });
221
-
222
- const mainPath = await getRepoRoot();
223
- if (wt.branch) {
224
- await git(["branch", "-D", wt.branch], mainPath);
225
- return `Removed worktree and branch: ${wt.branch}`;
226
- }
227
-
228
- return `Removed worktree: ${wt.path}`;
229
- }
230
-
231
- async function handleStatus(): Promise<string> {
232
- const worktrees = await worktree.list();
233
- const sections: string[] = [];
234
-
235
- for (const wt of worktrees) {
236
- const branch = wt.branch ?? "detached";
237
- const name = path.basename(wt.path);
238
-
239
- const statusResult = await git(["status", "--short"], wt.path);
240
- const status = statusResult.stdout.trim() || "(clean)";
241
-
242
- sections.push(`${name} (${branch})\n${"-".repeat(40)}\n${status}`);
243
- }
244
-
245
- return sections.join("\n\n");
246
- }
247
-
248
- async function handleSpawn(args: SpawnArgs, ctx: HookCommandContext): Promise<string> {
249
- const branch = args.name ?? `wt-agent-${nanoid(6)}`;
250
- const wt = await worktree.create(branch);
251
-
252
- const session = await createSession({
253
- branch,
254
- path: wt.path,
255
- scope: args.scope ? [args.scope] : undefined,
256
- task: args.task,
257
- });
258
- await updateSession(session.id, { status: "active" });
259
-
260
- const agent = await pickAgent(ctx.cwd);
261
- const context = args.scope ? `Scope: ${args.scope}` : undefined;
262
-
263
- // Command context doesn't expose a spawn API, so run the task subprocess directly.
264
- const result = await runSubprocess({
265
- cwd: wt.path,
266
- agent,
267
- task: args.task,
268
- index: 0,
269
- taskId: generateTaskName(),
270
- context,
271
- });
272
-
273
- await updateSession(session.id, {
274
- status: result.exitCode === 0 ? "completed" : "failed",
275
- completedAt: Date.now(),
276
- });
277
-
278
- if (result.exitCode !== 0) {
279
- return [
280
- `Agent failed in worktree: ${branch}`,
281
- result.stderr.trim() ? `Error: ${result.stderr.trim()}` : "Error: agent execution failed",
282
- "",
283
- "Actions:",
284
- ` /wt merge ${branch} - Apply changes to main`,
285
- " /wt status - Inspect changes",
286
- ` /wt rm ${branch} - Discard changes`,
287
- ].join("\n");
288
- }
289
-
290
- return [
291
- `Agent completed in worktree: ${branch}`,
292
- "",
293
- "Actions:",
294
- ` /wt merge ${branch} - Apply changes to main`,
295
- " /wt status - Inspect changes",
296
- ` /wt rm ${branch} - Discard changes`,
297
- ].join("\n");
298
- }
299
-
300
- async function handleParallel(args: ParallelTask[], ctx: HookCommandContext): Promise<string> {
301
- validateDisjointScopes(args.map((t) => t.scope));
302
-
303
- const sessionId = `parallel-${nanoid()}`;
304
- const agent = await pickAgent(ctx.cwd);
305
-
306
- const worktrees: Array<{ task: ParallelTask; wt: worktree.Worktree; session: worktree.WorktreeSession }> = [];
307
- for (let i = 0; i < args.length; i++) {
308
- const task = args[i];
309
- const branch = `wt-parallel-${sessionId}-${i}`;
310
- const wt = await worktree.create(branch);
311
- const session = await createSession({
312
- branch,
313
- path: wt.path,
314
- scope: [task.scope],
315
- task: task.task,
316
- });
317
- worktrees.push({ task, wt, session });
318
- }
319
-
320
- const agentPromises = worktrees.map(async ({ task, wt, session }, index) => {
321
- await updateSession(session.id, { status: "active" });
322
- const result = await runSubprocess({
323
- cwd: wt.path,
324
- agent,
325
- task: task.task,
326
- index,
327
- taskId: generateTaskName(),
328
- context: `Scope: ${task.scope}`,
329
- });
330
- await updateSession(session.id, {
331
- status: result.exitCode === 0 ? "completed" : "failed",
332
- completedAt: Date.now(),
333
- });
334
- return { wt, session, result };
335
- });
336
-
337
- const results = await Promise.all(agentPromises);
338
-
339
- const mergeResults: string[] = [];
340
-
341
- for (const { wt, session } of results) {
342
- try {
343
- await updateSession(session.id, { status: "merging" });
344
- const collapseResult = await collapse(wt.branch ?? wt.path, "main", {
345
- strategy: "simple",
346
- keepSource: false,
347
- });
348
- await updateSession(session.id, { status: "merged" });
349
- mergeResults.push(
350
- `ok ${wt.branch ?? path.basename(wt.path)}: +${collapseResult.insertions} -${collapseResult.deletions}`,
351
- );
352
- } catch (err) {
353
- await updateSession(session.id, { status: "failed" });
354
- mergeResults.push(`err ${wt.branch ?? path.basename(wt.path)}: ${formatError(err)}`);
355
- }
356
- }
357
-
358
- return [`Parallel execution complete (${args.length} agents)`, "", "Results:", ...mergeResults].join("\n");
359
- }
360
-
361
- export class WorktreeCommand implements CustomCommand {
362
- name = "wt";
363
- description = "Git worktree management";
364
-
365
- // biome-ignore lint/complexity/noUselessConstructor: interface conformance - loader passes API to all commands
366
- constructor(_api: CustomCommandAPI) {}
367
-
368
- async execute(args: string[], ctx: HookCommandContext): Promise<string | undefined> {
369
- if (args.length === 0) return formatUsage();
370
-
371
- const subcommand = args[0];
372
- const rest = args.slice(1);
373
-
374
- try {
375
- switch (subcommand) {
376
- case "new": {
377
- const parsed = parseFlags(rest);
378
- const branch = parsed.positionals[0];
379
- if (!branch) return formatUsage();
380
- const base = getFlagValue(parsed.flags, "base");
381
- if (parsed.flags.get("base") === true) {
382
- return "Missing value for --base";
383
- }
384
- return await handleNew({ branch, base });
385
- }
386
- case "list":
387
- return await handleList(ctx);
388
- case "merge": {
389
- const parsed = parseFlags(rest);
390
- const source = parsed.positionals[0];
391
- const target = parsed.positionals[1];
392
- if (!source) return formatUsage();
393
- const strategyRaw = getFlagValue(parsed.flags, "strategy");
394
- if (parsed.flags.get("strategy") === true) {
395
- return "Missing value for --strategy";
396
- }
397
- const strategy = strategyRaw as CollapseStrategy | undefined;
398
- const keep = getFlagBoolean(parsed.flags, "keep");
399
- return await handleMerge({ source, target, strategy, keep });
400
- }
401
- case "rm": {
402
- const parsed = parseFlags(rest);
403
- const name = parsed.positionals[0];
404
- if (!name) return formatUsage();
405
- const force = getFlagBoolean(parsed.flags, "force");
406
- return await handleRm({ name, force });
407
- }
408
- case "status":
409
- return await handleStatus();
410
- case "spawn": {
411
- const parsed = parseFlags(rest);
412
- const task = parsed.positionals[0];
413
- if (!task) return formatUsage();
414
- const scope = getFlagValue(parsed.flags, "scope");
415
- if (parsed.flags.get("scope") === true) {
416
- return "Missing value for --scope";
417
- }
418
- const name = getFlagValue(parsed.flags, "name");
419
- return await handleSpawn({ task, scope, name }, ctx);
420
- }
421
- case "parallel": {
422
- const tasks = parseParallelTasks(rest);
423
- if (tasks.length === 0) return formatUsage();
424
- return await handleParallel(tasks, ctx);
425
- }
426
- default:
427
- return formatUsage();
428
- }
429
- } catch (err) {
430
- return formatError(err);
431
- }
432
- }
433
- }
434
-
435
- export default WorktreeCommand;
@@ -1,213 +0,0 @@
1
- import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
- import { StringEnum } from "@oh-my-pi/pi-ai";
3
- import { type GitParams, gitTool as gitToolCore, type ToolResponse } from "@oh-my-pi/pi-git-tool";
4
- import { type Static, Type } from "@sinclair/typebox";
5
- import gitDescription from "../../prompts/tools/git.md" with { type: "text" };
6
- import { renderPromptTemplate } from "../prompt-templates";
7
- import type { ToolSession } from "./index";
8
-
9
- const gitSchema = Type.Object({
10
- operation: StringEnum([
11
- "status",
12
- "diff",
13
- "log",
14
- "show",
15
- "blame",
16
- "branch",
17
- "add",
18
- "restore",
19
- "commit",
20
- "checkout",
21
- "merge",
22
- "rebase",
23
- "stash",
24
- "cherry-pick",
25
- "fetch",
26
- "pull",
27
- "push",
28
- "tag",
29
- "pr",
30
- "issue",
31
- "ci",
32
- "release",
33
- ]),
34
-
35
- // Status
36
- only: Type.Optional(StringEnum(["branch", "modified", "staged", "untracked", "conflicts", "sync"])),
37
- ignored: Type.Optional(Type.Boolean()),
38
-
39
- // Diff
40
- target: Type.Optional(
41
- Type.Union([
42
- Type.Object({
43
- from: Type.String(),
44
- to: Type.Optional(Type.String()),
45
- }),
46
- Type.String(),
47
- ]),
48
- ),
49
- paths: Type.Optional(Type.Array(Type.String())),
50
- stat_only: Type.Optional(Type.Boolean()),
51
- name_only: Type.Optional(Type.Boolean()),
52
- context: Type.Optional(Type.Number()),
53
- max_lines: Type.Optional(Type.Number()),
54
- ignore_whitespace: Type.Optional(Type.Boolean()),
55
-
56
- // Log
57
- limit: Type.Optional(Type.Number()),
58
- ref: Type.Optional(Type.String()),
59
- author: Type.Optional(Type.String()),
60
- since: Type.Optional(Type.String()),
61
- until: Type.Optional(Type.String()),
62
- grep: Type.Optional(Type.String()),
63
- format: Type.Optional(StringEnum(["oneline", "short", "full"])),
64
- stat: Type.Optional(Type.Boolean()),
65
- merges: Type.Optional(Type.Boolean()),
66
- first_parent: Type.Optional(Type.Boolean()),
67
-
68
- // Show
69
- path: Type.Optional(Type.String()),
70
- diff: Type.Optional(Type.Boolean()),
71
- lines: Type.Optional(
72
- Type.Object({
73
- start: Type.Number(),
74
- end: Type.Number(),
75
- }),
76
- ),
77
-
78
- // Blame
79
- root: Type.Optional(Type.Boolean()),
80
-
81
- // Branch
82
- action: Type.Optional(StringEnum(["list", "create", "delete", "rename", "current"])),
83
- name: Type.Optional(Type.String()),
84
- newName: Type.Optional(Type.String()),
85
- startPoint: Type.Optional(Type.String()),
86
- remotes: Type.Optional(Type.Boolean()),
87
- force: Type.Optional(Type.Boolean()),
88
-
89
- // Add/Restore
90
- update: Type.Optional(Type.Boolean()),
91
- all: Type.Optional(Type.Boolean()),
92
- dry_run: Type.Optional(Type.Boolean()),
93
- staged: Type.Optional(Type.Boolean()),
94
- worktree: Type.Optional(Type.Boolean()),
95
- source: Type.Optional(Type.String()),
96
-
97
- // Commit
98
- message: Type.Optional(Type.String()),
99
- allow_empty: Type.Optional(Type.Boolean()),
100
- sign: Type.Optional(Type.Boolean()),
101
- no_verify: Type.Optional(Type.Boolean()),
102
- amend: Type.Optional(Type.Boolean()),
103
-
104
- // Checkout
105
- create: Type.Optional(Type.Boolean()),
106
-
107
- // Merge
108
- no_ff: Type.Optional(Type.Boolean()),
109
- ff_only: Type.Optional(Type.Boolean()),
110
- squash: Type.Optional(Type.Boolean()),
111
- abort: Type.Optional(Type.Boolean()),
112
- continue: Type.Optional(Type.Boolean()),
113
-
114
- // Rebase
115
- onto: Type.Optional(Type.String()),
116
- upstream: Type.Optional(Type.String()),
117
- skip: Type.Optional(Type.Boolean()),
118
-
119
- // Stash
120
- include_untracked: Type.Optional(Type.Boolean()),
121
- index: Type.Optional(Type.Number()),
122
- keep_index: Type.Optional(Type.Boolean()),
123
-
124
- // Cherry-pick
125
- commits: Type.Optional(Type.Array(Type.String())),
126
- no_commit: Type.Optional(Type.Boolean()),
127
-
128
- // Fetch/Pull/Push/Tag
129
- remote: Type.Optional(Type.String()),
130
- branch: Type.Optional(Type.String()),
131
- prune: Type.Optional(Type.Boolean()),
132
- tags: Type.Optional(Type.Boolean()),
133
- rebase: Type.Optional(Type.Boolean()),
134
- set_upstream: Type.Optional(Type.Boolean()),
135
- force_with_lease: Type.Optional(Type.Boolean()),
136
- delete: Type.Optional(Type.Boolean()),
137
- force_override: Type.Optional(Type.Boolean()),
138
-
139
- // Tag
140
- // (name/message/ref already covered)
141
-
142
- // PR
143
- number: Type.Optional(Type.Number()),
144
- title: Type.Optional(Type.String()),
145
- body: Type.Optional(Type.String()),
146
- base: Type.Optional(Type.String()),
147
- head: Type.Optional(Type.String()),
148
- draft: Type.Optional(Type.Boolean()),
149
- state: Type.Optional(StringEnum(["open", "closed", "merged", "all"])),
150
- merge_method: Type.Optional(StringEnum(["merge", "squash", "rebase"])),
151
- review_action: Type.Optional(StringEnum(["approve", "request-changes", "comment"])),
152
- review_body: Type.Optional(Type.String()),
153
-
154
- // Issue
155
- labels: Type.Optional(Type.Array(Type.String())),
156
- assignee: Type.Optional(Type.String()),
157
- comment_body: Type.Optional(Type.String()),
158
-
159
- // CI
160
- workflow: Type.Optional(Type.String()),
161
- run_id: Type.Optional(Type.Number()),
162
- inputs: Type.Optional(Type.Record(Type.String(), Type.String())),
163
- logs_failed: Type.Optional(Type.Boolean()),
164
-
165
- // Release
166
- notes: Type.Optional(Type.String()),
167
- generate_notes: Type.Optional(Type.Boolean()),
168
- prerelease: Type.Optional(Type.Boolean()),
169
- assets: Type.Optional(Type.Array(Type.String())),
170
- });
171
-
172
- export type GitToolDetails = ToolResponse<unknown>;
173
-
174
- export class GitTool implements AgentTool<typeof gitSchema, GitToolDetails> {
175
- public readonly name = "git";
176
- public readonly label = "Git";
177
- public readonly description: string;
178
- public readonly parameters = gitSchema;
179
-
180
- private readonly session: ToolSession;
181
-
182
- constructor(session: ToolSession) {
183
- this.session = session;
184
- this.description = renderPromptTemplate(gitDescription);
185
- }
186
-
187
- static createIf(session: ToolSession): GitTool | null {
188
- return session.settings?.getGitToolEnabled() === false ? null : new GitTool(session);
189
- }
190
-
191
- public async execute(
192
- _toolCallId: string,
193
- params: Static<typeof gitSchema>,
194
- _signal?: AbortSignal,
195
- _onUpdate?: AgentToolUpdateCallback<GitToolDetails>,
196
- _context?: AgentToolContext,
197
- ): Promise<AgentToolResult<GitToolDetails>> {
198
- if (params.operation === "commit" && !params.message) {
199
- throw new Error("Git commit requires a message to avoid an interactive editor. Provide `message`.");
200
- }
201
-
202
- const result = await gitToolCore(params as GitParams, this.session.cwd);
203
- if ("error" in result) {
204
- const message = result._rendered ?? result.error;
205
- return { content: [{ type: "text", text: message }], details: result };
206
- }
207
- if ("confirm" in result) {
208
- const message = result._rendered ?? result.confirm;
209
- return { content: [{ type: "text", text: message }], details: result };
210
- }
211
- return { content: [{ type: "text", text: result._rendered }], details: result };
212
- }
213
- }