@oh-my-pi/pi-coding-agent 6.8.5 → 6.9.69
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/CHANGELOG.md +51 -0
- package/package.json +6 -6
- package/src/cli/stats-cli.ts +191 -0
- package/src/core/agent-session.ts +103 -1
- package/src/core/extensions/index.ts +2 -0
- package/src/core/extensions/runner.ts +31 -0
- package/src/core/extensions/types.ts +24 -0
- package/src/core/messages.ts +48 -0
- package/src/core/sdk.ts +0 -2
- package/src/core/session-manager.ts +10 -1
- package/src/core/settings-manager.ts +0 -105
- package/src/core/tools/bash.ts +5 -7
- package/src/core/tools/index.ts +1 -5
- package/src/core/tools/patch/applicator.ts +115 -17
- package/src/core/tools/patch/index.ts +1 -1
- package/src/core/tools/patch/normalize.ts +185 -10
- package/src/core/tools/python.ts +444 -86
- package/src/core/tools/task/executor.ts +2 -6
- package/src/core/tools/task/index.ts +30 -12
- package/src/core/tools/task/render.ts +163 -30
- package/src/core/tools/task/template.ts +37 -0
- package/src/core/tools/task/types.ts +6 -2
- package/src/core/tools/task/worker.ts +1 -1
- package/src/index.ts +2 -2
- package/src/main.ts +12 -0
- package/src/modes/interactive/components/python-execution.ts +180 -0
- package/src/modes/interactive/components/settings-defs.ts +0 -70
- package/src/modes/interactive/components/settings-selector.ts +0 -1
- package/src/modes/interactive/components/welcome.ts +1 -0
- package/src/modes/interactive/controllers/command-controller.ts +46 -0
- package/src/modes/interactive/controllers/event-controller.ts +0 -11
- package/src/modes/interactive/controllers/input-controller.ts +28 -1
- package/src/modes/interactive/controllers/selector-controller.ts +0 -9
- package/src/modes/interactive/interactive-mode.ts +10 -58
- package/src/modes/interactive/theme/dark.json +2 -9
- package/src/modes/interactive/theme/defaults/alabaster.json +2 -8
- package/src/modes/interactive/theme/defaults/amethyst.json +2 -9
- package/src/modes/interactive/theme/defaults/anthracite.json +2 -9
- package/src/modes/interactive/theme/defaults/basalt.json +89 -88
- package/src/modes/interactive/theme/defaults/birch.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-abyss.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-arctic.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-aurora.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-catppuccin.json +2 -1
- package/src/modes/interactive/theme/defaults/dark-cavern.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-copper.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-cosmos.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-dracula.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-eclipse.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-ember.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-equinox.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-forest.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-github.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-gruvbox.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-lavender.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-lunar.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-midnight.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-monochrome.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-monokai.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-nebula.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-nord.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-ocean.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-one.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-rainforest.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-reef.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-retro.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-rose-pine.json +2 -1
- package/src/modes/interactive/theme/defaults/dark-sakura.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-slate.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-solarized.json +2 -1
- package/src/modes/interactive/theme/defaults/dark-solstice.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-starfall.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-sunset.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-swamp.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-synthwave.json +2 -1
- package/src/modes/interactive/theme/defaults/dark-taiga.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-terminal.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-tundra.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-twilight.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-volcanic.json +2 -8
- package/src/modes/interactive/theme/defaults/graphite.json +2 -9
- package/src/modes/interactive/theme/defaults/light-arctic.json +2 -1
- package/src/modes/interactive/theme/defaults/light-aurora-day.json +2 -8
- package/src/modes/interactive/theme/defaults/light-canyon.json +2 -8
- package/src/modes/interactive/theme/defaults/light-catppuccin.json +2 -1
- package/src/modes/interactive/theme/defaults/light-cirrus.json +2 -8
- package/src/modes/interactive/theme/defaults/light-coral.json +3 -2
- package/src/modes/interactive/theme/defaults/light-cyberpunk.json +2 -9
- package/src/modes/interactive/theme/defaults/light-dawn.json +2 -8
- package/src/modes/interactive/theme/defaults/light-dunes.json +2 -8
- package/src/modes/interactive/theme/defaults/light-eucalyptus.json +3 -2
- package/src/modes/interactive/theme/defaults/light-forest.json +2 -9
- package/src/modes/interactive/theme/defaults/light-frost.json +3 -2
- package/src/modes/interactive/theme/defaults/light-github.json +2 -1
- package/src/modes/interactive/theme/defaults/light-glacier.json +2 -8
- package/src/modes/interactive/theme/defaults/light-gruvbox.json +2 -9
- package/src/modes/interactive/theme/defaults/light-haze.json +2 -8
- package/src/modes/interactive/theme/defaults/light-honeycomb.json +3 -2
- package/src/modes/interactive/theme/defaults/light-lagoon.json +2 -8
- package/src/modes/interactive/theme/defaults/light-lavender.json +3 -2
- package/src/modes/interactive/theme/defaults/light-meadow.json +2 -8
- package/src/modes/interactive/theme/defaults/light-mint.json +3 -2
- package/src/modes/interactive/theme/defaults/light-monochrome.json +2 -1
- package/src/modes/interactive/theme/defaults/light-ocean.json +2 -9
- package/src/modes/interactive/theme/defaults/light-one.json +2 -8
- package/src/modes/interactive/theme/defaults/light-opal.json +2 -8
- package/src/modes/interactive/theme/defaults/light-orchard.json +2 -8
- package/src/modes/interactive/theme/defaults/light-paper.json +3 -2
- package/src/modes/interactive/theme/defaults/light-prism.json +2 -8
- package/src/modes/interactive/theme/defaults/light-retro.json +2 -9
- package/src/modes/interactive/theme/defaults/light-sand.json +3 -2
- package/src/modes/interactive/theme/defaults/light-savanna.json +2 -8
- package/src/modes/interactive/theme/defaults/light-solarized.json +2 -1
- package/src/modes/interactive/theme/defaults/light-soleil.json +2 -8
- package/src/modes/interactive/theme/defaults/light-sunset.json +2 -9
- package/src/modes/interactive/theme/defaults/light-synthwave.json +2 -9
- package/src/modes/interactive/theme/defaults/light-tokyo-night.json +2 -9
- package/src/modes/interactive/theme/defaults/light-wetland.json +2 -8
- package/src/modes/interactive/theme/defaults/light-zenith.json +2 -8
- package/src/modes/interactive/theme/defaults/limestone.json +2 -8
- package/src/modes/interactive/theme/defaults/mahogany.json +2 -9
- package/src/modes/interactive/theme/defaults/marble.json +2 -8
- package/src/modes/interactive/theme/defaults/obsidian.json +89 -88
- package/src/modes/interactive/theme/defaults/onyx.json +89 -88
- package/src/modes/interactive/theme/defaults/pearl.json +2 -8
- package/src/modes/interactive/theme/defaults/porcelain.json +89 -88
- package/src/modes/interactive/theme/defaults/quartz.json +2 -8
- package/src/modes/interactive/theme/defaults/sandstone.json +2 -8
- package/src/modes/interactive/theme/defaults/titanium.json +88 -87
- package/src/modes/interactive/theme/light.json +2 -8
- package/src/modes/interactive/theme/theme-schema.json +5 -0
- package/src/modes/interactive/theme/theme.ts +7 -0
- package/src/modes/interactive/types.ts +5 -15
- package/src/modes/interactive/utils/ui-helpers.ts +20 -0
- package/src/prompts/system/system-prompt.md +8 -0
- package/src/prompts/tools/python.md +40 -2
- package/src/prompts/tools/task.md +8 -13
- package/src/core/custom-commands/bundled/wt/index.ts +0 -435
- package/src/core/tools/git.ts +0 -213
- package/src/core/voice-controller.ts +0 -135
- package/src/core/voice-supervisor.ts +0 -976
- package/src/core/voice.ts +0 -314
- package/src/lib/worktree/collapse.ts +0 -180
- package/src/lib/worktree/constants.ts +0 -14
- package/src/lib/worktree/errors.ts +0 -23
- package/src/lib/worktree/git.ts +0 -60
- package/src/lib/worktree/index.ts +0 -15
- package/src/lib/worktree/operations.ts +0 -216
- package/src/lib/worktree/session.ts +0 -114
- package/src/lib/worktree/stats.ts +0 -67
- package/src/modes/interactive/utils/voice-manager.ts +0 -96
- package/src/prompts/tools/git.md +0 -9
- package/src/prompts/voice-summary.md +0 -12
|
@@ -10,6 +10,7 @@ import { BranchSummaryMessageComponent } from "../components/branch-summary-mess
|
|
|
10
10
|
import { CompactionSummaryMessageComponent } from "../components/compaction-summary-message";
|
|
11
11
|
import { CustomMessageComponent } from "../components/custom-message";
|
|
12
12
|
import { DynamicBorder } from "../components/dynamic-border";
|
|
13
|
+
import { PythonExecutionComponent } from "../components/python-execution";
|
|
13
14
|
import { ReadToolGroupComponent } from "../components/read-tool-group";
|
|
14
15
|
import { ToolExecutionComponent } from "../components/tool-execution";
|
|
15
16
|
import { UserMessageComponent } from "../components/user-message";
|
|
@@ -83,6 +84,20 @@ export class UiHelpers {
|
|
|
83
84
|
this.ctx.chatContainer.addChild(component);
|
|
84
85
|
break;
|
|
85
86
|
}
|
|
87
|
+
case "pythonExecution": {
|
|
88
|
+
const component = new PythonExecutionComponent(message.code, this.ctx.ui, message.excludeFromContext);
|
|
89
|
+
if (message.output) {
|
|
90
|
+
component.appendOutput(message.output);
|
|
91
|
+
}
|
|
92
|
+
component.setComplete(
|
|
93
|
+
message.exitCode,
|
|
94
|
+
message.cancelled,
|
|
95
|
+
message.truncated ? ({ truncated: true } as TruncationResult) : undefined,
|
|
96
|
+
message.fullOutputPath,
|
|
97
|
+
);
|
|
98
|
+
this.ctx.chatContainer.addChild(component);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
86
101
|
case "hookMessage":
|
|
87
102
|
case "custom": {
|
|
88
103
|
if (message.display) {
|
|
@@ -461,6 +476,11 @@ export class UiHelpers {
|
|
|
461
476
|
this.ctx.chatContainer.addChild(component);
|
|
462
477
|
}
|
|
463
478
|
this.ctx.pendingBashComponents = [];
|
|
479
|
+
for (const component of this.ctx.pendingPythonComponents) {
|
|
480
|
+
this.ctx.pendingMessagesContainer.removeChild(component);
|
|
481
|
+
this.ctx.chatContainer.addChild(component);
|
|
482
|
+
}
|
|
483
|
+
this.ctx.pendingPythonComponents = [];
|
|
464
484
|
}
|
|
465
485
|
|
|
466
486
|
findLastAssistantMessage(): AssistantMessage | undefined {
|
|
@@ -200,6 +200,13 @@ Do not open a file hoping to find something. Know where to look first.
|
|
|
200
200
|
- When summarizing: plain text, file paths. Do not echo content back.
|
|
201
201
|
{{/ifAny}}
|
|
202
202
|
- Be brief. Show file paths clearly.
|
|
203
|
+
{{#has tools "ask"}}
|
|
204
|
+
|
|
205
|
+
### Concurrent work
|
|
206
|
+
Other agents or the user may be editing files concurrently.
|
|
207
|
+
When file contents differ from expectations or edits fail: re-read and adapt.
|
|
208
|
+
**Ask before** `git checkout/restore/reset`, bulk overwrites, or deleting code you didn't write.
|
|
209
|
+
{{/has}}
|
|
203
210
|
</practice>
|
|
204
211
|
|
|
205
212
|
<method>
|
|
@@ -324,6 +331,7 @@ Keep going until finished.
|
|
|
324
331
|
- Do not write code before stating assumptions.
|
|
325
332
|
- Do not claim correctness you haven't verified.
|
|
326
333
|
- Do not handle only the happy path.
|
|
334
|
+
{{#has tools "ask"}}- If files differ from expectations, ask before discarding uncommitted work.{{/has}}
|
|
327
335
|
|
|
328
336
|
|
|
329
337
|
Let edge cases surface before you handle them. Let the failure modes exist in your mind before you prevent them. Let the code be smaller than your first instinct.
|
|
@@ -1,4 +1,42 @@
|
|
|
1
|
-
Executes Python
|
|
1
|
+
Executes Python cells sequentially in a persistent IPython kernel.
|
|
2
|
+
|
|
3
|
+
## How to use (REPL discipline)
|
|
4
|
+
|
|
5
|
+
The kernel persists between calls and between cells. **Imports, variables, and functions survive.** Use this.
|
|
6
|
+
|
|
7
|
+
**Work incrementally:**
|
|
8
|
+
- One logical step per cell (imports, define a function, test it, use it)
|
|
9
|
+
- Pass multiple small cells in one call—they execute sequentially
|
|
10
|
+
- Define small functions you can reuse and debug individually
|
|
11
|
+
- Put explanations in the assistant message or cell title, **not** inside code
|
|
12
|
+
|
|
13
|
+
**When something fails:**
|
|
14
|
+
- The error tells you which cell failed (e.g., "Cell 3 failed")
|
|
15
|
+
- Earlier cells already ran—their state persists in the kernel
|
|
16
|
+
- Resubmit with only the fixed cell (or the fixed cell + remaining cells)
|
|
17
|
+
- Do NOT rewrite working cells or re-import modules
|
|
18
|
+
|
|
19
|
+
**Anti-patterns to avoid:**
|
|
20
|
+
- Putting everything in one giant cell
|
|
21
|
+
- Re-importing modules you already imported
|
|
22
|
+
- Rewriting working code when only one part failed
|
|
23
|
+
- Large functions that are hard to debug piece by piece
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
# BAD: One giant cell
|
|
27
|
+
cells: [{
|
|
28
|
+
"title": "all-in-one",
|
|
29
|
+
"code": "import json\nfrom pathlib import Path\ndef process_all_files():\n # 50 lines...\n pass\nresult = process_all_files()"
|
|
30
|
+
}]
|
|
31
|
+
|
|
32
|
+
# GOOD: Multiple small cells
|
|
33
|
+
cells: [
|
|
34
|
+
{"title": "imports", "code": "import json\nfrom pathlib import Path"},
|
|
35
|
+
{"title": "parse helper", "code": "def parse_config(path):\n return json.loads(Path(path).read_text())"},
|
|
36
|
+
{"title": "test helper", "code": "parse_config('config.json')"},
|
|
37
|
+
{"title": "use helper", "code": "configs = [parse_config(p) for p in Path('.').glob('*.json')]"}
|
|
38
|
+
]
|
|
39
|
+
```
|
|
2
40
|
|
|
3
41
|
## When to use Python
|
|
4
42
|
|
|
@@ -75,7 +113,7 @@ cols(read("data.tsv"), 0, 2, sep="\t")
|
|
|
75
113
|
|
|
76
114
|
- Code executes as IPython cells; users see the full cell output (including rendered figures, tables, etc.)
|
|
77
115
|
- Kernel persists for the session by default; per-call mode uses a fresh kernel each call. Use `reset: true` to clear state when session mode is active
|
|
78
|
-
- Use `
|
|
116
|
+
- Use `cwd` parameter instead of `os.chdir()` in tool call
|
|
79
117
|
- Use `plt.show()` to display figures
|
|
80
118
|
- Use `display()` from IPython.display for rich output (HTML, Markdown, images, etc.)
|
|
81
119
|
- Output streams in real time, truncated after 50KB
|
|
@@ -49,12 +49,12 @@ Agents with "Output: structured" have a fixed schema enforced via frontmatter; y
|
|
|
49
49
|
## Parameters
|
|
50
50
|
|
|
51
51
|
- `agent`: Agent type to use for all tasks
|
|
52
|
-
- `context`:
|
|
53
|
-
- `model`: (optional) Model override (fuzzy matching, e.g., "sonnet", "opus")
|
|
54
|
-
- `tasks`: Array of `{id,
|
|
52
|
+
- `context`: Template with `{{placeholders}}` for multi-task. Each placeholder is filled from task vars.
|
|
53
|
+
- `model`: (optional) Model override for all tasks (fuzzy matching, e.g., "sonnet", "opus")
|
|
54
|
+
- `tasks`: Array of `{id, description, vars}` - tasks to run in parallel (max {{MAX_PARALLEL_TASKS}}, {{MAX_CONCURRENCY}} concurrent)
|
|
55
55
|
- `id`: Short CamelCase identifier for display (max 20 chars, e.g., "SessionStore", "LspRefactor")
|
|
56
|
-
- `task`: The task prompt for the agent
|
|
57
56
|
- `description`: Short human-readable description of what the task does
|
|
57
|
+
- `vars`: Object with keys matching `{{placeholders}}` in context
|
|
58
58
|
- `output`: (optional) JTD schema for structured subagent output (used by the complete tool)
|
|
59
59
|
|
|
60
60
|
## Example
|
|
@@ -65,7 +65,7 @@ assistant: I'll execute the refactoring plan.
|
|
|
65
65
|
assistant: Uses the Task tool:
|
|
66
66
|
{
|
|
67
67
|
"agent": "task",
|
|
68
|
-
"context": "Refactoring the auth module into separate concerns.\n\nPlan:\n1. AuthProvider - Extract React context and provider from src/auth/index.tsx\n2. AuthApi - Extract API calls to src/auth/api.ts, use existing fetchJson helper\n3. AuthTypes - Move types to
|
|
68
|
+
"context": "Refactoring the auth module into separate concerns.\n\nPlan:\n1. AuthProvider - Extract React context and provider from src/auth/index.tsx\n2. AuthApi - Extract API calls to src/auth/api.ts, use existing fetchJson helper\n3. AuthTypes - Move types to types.ts, re-export from index\n\nConstraints:\n- Preserve all existing exports from src/auth/index.tsx\n- Use project's fetchJson (src/utils/http.ts), don't use raw fetch\n- No new dependencies\n\nTask: {{step}}\n\nFiles: {{files}}",
|
|
69
69
|
"output": {
|
|
70
70
|
"properties": {
|
|
71
71
|
"summary": { "type": "string" },
|
|
@@ -74,14 +74,9 @@ assistant: Uses the Task tool:
|
|
|
74
74
|
}
|
|
75
75
|
},
|
|
76
76
|
"tasks": [
|
|
77
|
-
{ "id": "AuthProvider", "
|
|
78
|
-
{ "id": "AuthApi", "
|
|
79
|
-
{ "id": "AuthTypes", "
|
|
77
|
+
{ "id": "AuthProvider", "description": "Extract React context", "vars": { "step": "Execute step 1: Extract AuthProvider and AuthContext", "files": "src/auth/index.tsx" } },
|
|
78
|
+
{ "id": "AuthApi", "description": "Extract API layer", "vars": { "step": "Execute step 2: Extract API calls to api.ts", "files": "src/auth/api.ts" } },
|
|
79
|
+
{ "id": "AuthTypes", "description": "Extract types", "vars": { "step": "Execute step 3: Move types to types.ts", "files": "src/auth/types.ts" } }
|
|
80
80
|
]
|
|
81
81
|
}
|
|
82
82
|
</example>
|
|
83
|
-
|
|
84
|
-
Key points:
|
|
85
|
-
- **Plan in context**: The full plan is written once; each task references its step without repeating shared constraints
|
|
86
|
-
- **Parallel execution**: 3 agents run concurrently, each owning one step - no duplicated work
|
|
87
|
-
- **Structured output**: JTD schema ensures consistent reporting across all agents
|
|
@@ -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;
|