@sven1103/opencode-worktree-workflow 0.5.0 → 0.6.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 CHANGED
@@ -1,6 +1,46 @@
1
1
  # OpenCode Worktree Workflow
2
2
 
3
- `@sven1103/opencode-worktree-workflow` is an OpenCode plugin that adds git worktree helpers for creating synced feature worktrees and cleaning up merged ones.
3
+ `@sven1103/opencode-worktree-workflow` is an npm package that provides OpenCode git worktree helpers for creating synced feature worktrees and cleaning up merged ones.
4
+
5
+ ## Quick start
6
+
7
+ To get the workflow running in a project:
8
+
9
+ 1. Install the package once by following [Recommended setup](#recommended-setup).
10
+ 2. Enable the plugin in your OpenCode config as shown in [Recommended setup](#recommended-setup).
11
+ 3. If you want manual `/wt-new` and `/wt-clean` triggers, install the markdown files from [Install slash commands](#install-slash-commands).
12
+ 4. If you want policy guidance for when to isolate work, install the skill from [Co-shipped skill](#co-shipped-skill).
13
+ 5. If you need to understand how the local fallback works, see [CLI fallback](#cli-fallback).
14
+
15
+ ## Recommended setup
16
+
17
+ Install the package once:
18
+
19
+ ```sh
20
+ npm install -D @sven1103/opencode-worktree-workflow
21
+ ```
22
+
23
+ Enable the native OpenCode plugin in `opencode.json`:
24
+
25
+ ```json
26
+ {
27
+ "$schema": "https://opencode.ai/config.json",
28
+ "plugin": ["@sven1103/opencode-worktree-workflow"]
29
+ }
30
+ ```
31
+
32
+ This single package provides two access modes:
33
+
34
+ - native plugin tools inside OpenCode: `worktree_prepare`, `worktree_cleanup`
35
+ - local CLI fallback from the same installed package:
36
+ - `npx opencode-worktree-workflow wt-new "<title>" --json`
37
+ - `npx opencode-worktree-workflow wt-clean <args> --json`
38
+
39
+ In practice:
40
+
41
+ - if the plugin is loaded, use the native tools first
42
+ - if the native tools are unavailable, use the local CLI fallback from the same installed package
43
+ - if the package is not installed, no CLI fallback is available
4
44
 
5
45
  ## Install in an OpenCode project
6
46
 
@@ -28,7 +68,7 @@ Keeping the npm dependency in `package.json` makes the installation more durable
28
68
  If you do not already install dependencies in your project, you can add the package directly with npm:
29
69
 
30
70
  ```sh
31
- npm install @sven1103/opencode-worktree-workflow
71
+ npm install -D @sven1103/opencode-worktree-workflow
32
72
  ```
33
73
 
34
74
  ## Install slash commands
@@ -75,12 +115,76 @@ curl -fsSL "https://github.com/sven1103-agent/opencode-worktree-plugin/releases/
75
115
  curl -fsSL "https://github.com/sven1103-agent/opencode-worktree-plugin/releases/download/${VERSION}/wt-clean.md" -o ".opencode/commands/wt-clean.md"
76
116
  ```
77
117
 
118
+ ## Co-shipped skill
119
+
120
+ This repo also co-ships a `worktree-workflow` skill as a policy layer over the package capability.
121
+
122
+ - checked-in skill: `skills/worktree-workflow/SKILL.md`
123
+ - release asset: `SKILL.md`
124
+
125
+ The skill teaches when to use task-scoped worktrees, when repo root is still acceptable, and how to prefer the native tool path before falling back to the packaged CLI.
126
+
127
+ Project-local install (latest release):
128
+
129
+ ```sh
130
+ mkdir -p .opencode/skills/worktree-workflow
131
+ curl -fsSL "https://github.com/sven1103-agent/opencode-worktree-plugin/releases/latest/download/SKILL.md" -o ".opencode/skills/worktree-workflow/SKILL.md"
132
+ ```
133
+
134
+ ```sh
135
+ mkdir -p .opencode/skills/worktree-workflow
136
+ wget -qO ".opencode/skills/worktree-workflow/SKILL.md" "https://github.com/sven1103-agent/opencode-worktree-plugin/releases/latest/download/SKILL.md"
137
+ ```
138
+
139
+ If your setup uses installed skill files, copy the released `SKILL.md` into a `worktree-workflow/` skill folder in the appropriate location for that environment, or consume the checked-in file from this repo directly.
140
+
78
141
  ## What the plugin provides
79
142
 
80
143
  - `worktree_prepare`: create a worktree and matching branch from the latest configured base-branch commit, or the default branch when no base branch is configured
81
144
  - `worktree_cleanup`: preview all connected worktrees against the configured base branch, auto-clean safe ones, and optionally remove selected review items
82
145
 
83
- This package currently focuses on plugin distribution. Slash command packaging can be layered on later.
146
+ This package now ships the plugin capability, a CLI fallback surface, thin slash commands, and a co-shipped policy skill.
147
+
148
+ ## Structured contract
149
+
150
+ The native tool results and CLI `--json` output now use a versioned structured contract with a `schema_version` field.
151
+
152
+ - current `schema_version`: `1.0.0`
153
+ - contract overview: `docs/contract.md`
154
+ - compatibility model: `docs/compatibility.md`
155
+ - checked-in schemas for transparency:
156
+ - `schemas/worktree-prepare.result.schema.json`
157
+ - `schemas/worktree-cleanup-preview.result.schema.json`
158
+ - `schemas/worktree-cleanup-apply.result.schema.json`
159
+
160
+ Human-readable output remains available through the result `message`, but callers should depend on the structured fields rather than parsing prose.
161
+
162
+ ## CLI fallback
163
+
164
+ The npm package also exposes a local CLI so agents can fall back to the same installed package when the native plugin tools are unavailable.
165
+
166
+ Examples:
167
+
168
+ ```sh
169
+ npx opencode-worktree-workflow wt-new "Improve checkout retry logic"
170
+ npx opencode-worktree-workflow wt-new "Improve checkout retry logic" --json
171
+ npx opencode-worktree-workflow wt-clean preview
172
+ npx opencode-worktree-workflow wt-clean apply feature/foo --json
173
+ ```
174
+
175
+ Defaults:
176
+
177
+ - human-readable output by default
178
+ - structured output with `--json`
179
+ - the CLI shares the same underlying implementation and result contract as the native tools
180
+ - the CLI fallback depends on the package already being installed in the project
181
+
182
+ ## Compatibility model
183
+
184
+ The repo keeps config loading, argument normalization, and execution semantics centralized in the package implementation so existing installations continue to work across native tools, CLI fallback, and slash commands.
185
+
186
+ - compatibility overview: `docs/compatibility.md`
187
+ - existing `.opencode/worktree-workflow.json` setups remain the supported configuration path
84
188
 
85
189
  ## Optional project configuration
86
190
 
package/package.json CHANGED
@@ -1,14 +1,19 @@
1
1
  {
2
2
  "name": "@sven1103/opencode-worktree-workflow",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "OpenCode plugin for creating and cleaning up git worktrees.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
+ "bin": {
8
+ "opencode-worktree-workflow": "./src/cli.js"
9
+ },
7
10
  "exports": {
8
11
  ".": "./src/index.js"
9
12
  },
10
13
  "files": [
11
- "src"
14
+ "src",
15
+ "schemas",
16
+ "skills"
12
17
  ],
13
18
  "keywords": [
14
19
  "opencode",
@@ -0,0 +1,185 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/sven1103-agent/opencode-worktree-plugin/schemas/worktree-cleanup-apply.result.schema.json",
4
+ "title": "worktree_cleanup apply result",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schema_version",
9
+ "ok",
10
+ "mode",
11
+ "default_branch",
12
+ "base_branch",
13
+ "base_ref",
14
+ "requested_selectors",
15
+ "removed",
16
+ "failed"
17
+ ],
18
+ "properties": {
19
+ "schema_version": {
20
+ "type": "string",
21
+ "const": "1.0.0"
22
+ },
23
+ "ok": {
24
+ "type": "boolean",
25
+ "const": true
26
+ },
27
+ "mode": {
28
+ "type": "string",
29
+ "const": "apply"
30
+ },
31
+ "default_branch": {
32
+ "type": "string",
33
+ "minLength": 1
34
+ },
35
+ "base_branch": {
36
+ "type": "string",
37
+ "minLength": 1
38
+ },
39
+ "base_ref": {
40
+ "type": "string",
41
+ "minLength": 1
42
+ },
43
+ "requested_selectors": {
44
+ "type": "array",
45
+ "items": {
46
+ "type": "string"
47
+ }
48
+ },
49
+ "removed": {
50
+ "type": "array",
51
+ "items": {
52
+ "$ref": "#/$defs/removedItem"
53
+ }
54
+ },
55
+ "failed": {
56
+ "type": "array",
57
+ "items": {
58
+ "$ref": "#/$defs/failedItem"
59
+ }
60
+ },
61
+ "message": {
62
+ "type": "string"
63
+ }
64
+ },
65
+ "$defs": {
66
+ "baseCleanupItem": {
67
+ "type": "object",
68
+ "required": [
69
+ "branch",
70
+ "worktree_path",
71
+ "head",
72
+ "status",
73
+ "reason",
74
+ "detached",
75
+ "selectable"
76
+ ],
77
+ "properties": {
78
+ "branch": {
79
+ "type": ["string", "null"]
80
+ },
81
+ "worktree_path": {
82
+ "type": ["string", "null"]
83
+ },
84
+ "head": {
85
+ "type": ["string", "null"]
86
+ },
87
+ "status": {
88
+ "type": ["string", "null"],
89
+ "enum": ["safe", "review", "blocked", null]
90
+ },
91
+ "reason": {
92
+ "type": ["string", "null"]
93
+ },
94
+ "detached": {
95
+ "type": "boolean"
96
+ },
97
+ "selectable": {
98
+ "type": ["boolean", "null"]
99
+ }
100
+ }
101
+ },
102
+ "removedItem": {
103
+ "type": "object",
104
+ "additionalProperties": false,
105
+ "required": [
106
+ "branch",
107
+ "worktree_path",
108
+ "head",
109
+ "status",
110
+ "reason",
111
+ "detached",
112
+ "selectable",
113
+ "selected"
114
+ ],
115
+ "properties": {
116
+ "branch": {
117
+ "type": ["string", "null"]
118
+ },
119
+ "worktree_path": {
120
+ "type": ["string", "null"]
121
+ },
122
+ "head": {
123
+ "type": ["string", "null"]
124
+ },
125
+ "status": {
126
+ "type": ["string", "null"],
127
+ "enum": ["safe", "review", "blocked", null]
128
+ },
129
+ "reason": {
130
+ "type": ["string", "null"]
131
+ },
132
+ "detached": {
133
+ "type": "boolean"
134
+ },
135
+ "selectable": {
136
+ "type": ["boolean", "null"]
137
+ },
138
+ "selected": {
139
+ "type": "boolean"
140
+ }
141
+ }
142
+ },
143
+ "failedItem": {
144
+ "type": "object",
145
+ "additionalProperties": false,
146
+ "required": [
147
+ "selector",
148
+ "branch",
149
+ "worktree_path",
150
+ "head",
151
+ "status",
152
+ "reason",
153
+ "detached",
154
+ "selectable"
155
+ ],
156
+ "properties": {
157
+ "selector": {
158
+ "type": ["string", "null"]
159
+ },
160
+ "branch": {
161
+ "type": ["string", "null"]
162
+ },
163
+ "worktree_path": {
164
+ "type": ["string", "null"]
165
+ },
166
+ "head": {
167
+ "type": ["string", "null"]
168
+ },
169
+ "status": {
170
+ "type": ["string", "null"],
171
+ "enum": ["safe", "review", "blocked", null]
172
+ },
173
+ "reason": {
174
+ "type": ["string", "null"]
175
+ },
176
+ "detached": {
177
+ "type": "boolean"
178
+ },
179
+ "selectable": {
180
+ "type": ["boolean", "null"]
181
+ }
182
+ }
183
+ }
184
+ }
185
+ }
@@ -0,0 +1,109 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/sven1103-agent/opencode-worktree-plugin/schemas/worktree-cleanup-preview.result.schema.json",
4
+ "title": "worktree_cleanup preview result",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schema_version",
9
+ "ok",
10
+ "mode",
11
+ "default_branch",
12
+ "base_branch",
13
+ "base_ref",
14
+ "groups"
15
+ ],
16
+ "properties": {
17
+ "schema_version": {
18
+ "type": "string",
19
+ "const": "1.0.0"
20
+ },
21
+ "ok": {
22
+ "type": "boolean",
23
+ "const": true
24
+ },
25
+ "mode": {
26
+ "type": "string",
27
+ "const": "preview"
28
+ },
29
+ "default_branch": {
30
+ "type": "string",
31
+ "minLength": 1
32
+ },
33
+ "base_branch": {
34
+ "type": "string",
35
+ "minLength": 1
36
+ },
37
+ "base_ref": {
38
+ "type": "string",
39
+ "minLength": 1
40
+ },
41
+ "groups": {
42
+ "type": "object",
43
+ "additionalProperties": false,
44
+ "required": ["safe", "review", "blocked"],
45
+ "properties": {
46
+ "safe": {
47
+ "type": "array",
48
+ "items": {
49
+ "$ref": "#/$defs/cleanupItem"
50
+ }
51
+ },
52
+ "review": {
53
+ "type": "array",
54
+ "items": {
55
+ "$ref": "#/$defs/cleanupItem"
56
+ }
57
+ },
58
+ "blocked": {
59
+ "type": "array",
60
+ "items": {
61
+ "$ref": "#/$defs/cleanupItem"
62
+ }
63
+ }
64
+ }
65
+ },
66
+ "message": {
67
+ "type": "string"
68
+ }
69
+ },
70
+ "$defs": {
71
+ "cleanupItem": {
72
+ "type": "object",
73
+ "additionalProperties": false,
74
+ "required": [
75
+ "branch",
76
+ "worktree_path",
77
+ "head",
78
+ "status",
79
+ "reason",
80
+ "detached",
81
+ "selectable"
82
+ ],
83
+ "properties": {
84
+ "branch": {
85
+ "type": ["string", "null"]
86
+ },
87
+ "worktree_path": {
88
+ "type": ["string", "null"]
89
+ },
90
+ "head": {
91
+ "type": ["string", "null"]
92
+ },
93
+ "status": {
94
+ "type": ["string", "null"],
95
+ "enum": ["safe", "review", "blocked", null]
96
+ },
97
+ "reason": {
98
+ "type": ["string", "null"]
99
+ },
100
+ "detached": {
101
+ "type": "boolean"
102
+ },
103
+ "selectable": {
104
+ "type": ["boolean", "null"]
105
+ }
106
+ }
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,64 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/sven1103-agent/opencode-worktree-plugin/schemas/worktree-prepare.result.schema.json",
4
+ "title": "worktree_prepare result",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schema_version",
9
+ "ok",
10
+ "title",
11
+ "branch",
12
+ "worktree_path",
13
+ "default_branch",
14
+ "base_branch",
15
+ "base_ref",
16
+ "base_commit",
17
+ "created"
18
+ ],
19
+ "properties": {
20
+ "schema_version": {
21
+ "type": "string",
22
+ "const": "1.0.0"
23
+ },
24
+ "ok": {
25
+ "type": "boolean",
26
+ "const": true
27
+ },
28
+ "title": {
29
+ "type": "string",
30
+ "minLength": 1
31
+ },
32
+ "branch": {
33
+ "type": "string",
34
+ "minLength": 1
35
+ },
36
+ "worktree_path": {
37
+ "type": "string",
38
+ "minLength": 1
39
+ },
40
+ "default_branch": {
41
+ "type": "string",
42
+ "minLength": 1
43
+ },
44
+ "base_branch": {
45
+ "type": "string",
46
+ "minLength": 1
47
+ },
48
+ "base_ref": {
49
+ "type": "string",
50
+ "minLength": 1
51
+ },
52
+ "base_commit": {
53
+ "type": "string",
54
+ "minLength": 1
55
+ },
56
+ "created": {
57
+ "type": "boolean",
58
+ "const": true
59
+ },
60
+ "message": {
61
+ "type": "string"
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: worktree-workflow
3
+ description: Use this skill when you need to decide whether a task should move into a git worktree, when repo root is still safe, or when you need to choose between native worktree tools and the standard CLI fallback.
4
+ ---
5
+
6
+ ## When to use me
7
+
8
+ - Use this skill when work should be isolated from the current checkout.
9
+ - Use this skill when the task is substantial, risky, or likely to involve multiple collaborating agents.
10
+ - Use this skill when you need to decide whether repo root is still safe for a tiny edit.
11
+ - Use this skill when you need to choose the native worktree tools first and the packaged CLI fallback second.
12
+
13
+ ## Goals
14
+
15
+ - Keep user work isolated from repo root when appropriate.
16
+ - Prefer task-scoped worktrees for non-trivial editable work.
17
+ - Support both native worktree tools and CLI fallback environments.
18
+ - Keep policy in the skill and execution semantics in the package.
19
+
20
+ ## Root policy
21
+
22
+ - Treat repo root as shared space.
23
+ - Use repo root only for tiny, root-safe tasks.
24
+ - Treat a task as root-safe only when it is one focused change, touches at most one or two closely related files, does not need parallel delegation, does not imply a likely edit-test-fix loop, does not involve risky refactoring or migration work, and does not risk interfering with unrelated dirty root state.
25
+
26
+ ## Task worktree policy
27
+
28
+ - Prefer one task-scoped worktree for non-trivial editable work.
29
+ - Treat a task worktree as belonging to a task or workstream, not to a single agent.
30
+ - Keep planning, implementation, and review for one linear task in the same task worktree unless the work splits.
31
+ - Create a separate divergent worktree only when concurrent branches of work may conflict or need independent experimentation.
32
+
33
+ ## Capability ladder
34
+
35
+ - Use the native worktree tools as the primary path when the native worktree tools are available.
36
+ - Use the packaged CLI fallback path when the native tools are unavailable.
37
+ - Continue in repo root only for tiny, root-safe tasks when no worktree capability is available; otherwise stop and explain that isolation capability is unavailable.
38
+
39
+ ## Creation behavior
40
+
41
+ - Use a short descriptive task title when creating a worktree.
42
+ - Treat the returned `worktree_path` as the active execution target for follow-up work.
43
+ - Use that worktree path as the working directory for later shell commands.
44
+ - Use paths inside that worktree for later file reads or edits.
45
+
46
+ ## Cleanup behavior
47
+
48
+ - Keep cleanup preview-first by default because cleanup is preview-first unless deletion is clearly intended.
49
+ - Use cleanup apply only when deletion is clearly intended and controlled by the orchestrating runtime.
50
+ - Treat slash commands as manual human entry points, not as the canonical agent interface.
51
+
52
+ ## Boundaries
53
+
54
+ - Do not encode runtime storage, session artifact, or orchestration file-layout details here.
55
+ - Do not duplicate package argument normalization or config parsing here.
56
+ - Rely on the shared package implementation for config loading, base-branch resolution, cleanup normalization, and structured result semantics.
57
+
58
+ ## Examples
59
+
60
+ - Move a risky refactor into a task-scoped worktree before editing multiple files and running several test-fix loops.
61
+ - Stay in repo root for a tiny, root-safe doc fix that touches one related file and does not need delegation.
62
+ - Prefer the native worktree tools first, then switch to the packaged CLI fallback path if the native tools are unavailable.
package/src/cli.js ADDED
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFile } from "node:child_process";
4
+ import { fileURLToPath } from "node:url";
5
+ import { promisify } from "node:util";
6
+
7
+ import { WorktreeWorkflowPlugin } from "./index.js";
8
+
9
+ const execFileAsync = promisify(execFile);
10
+
11
+ function shellEscape(value) {
12
+ return `'${String(value).replaceAll("'", `'"'"'`)}'`;
13
+ }
14
+
15
+ function createShell(cwdBase) {
16
+ const shell = (strings, ...values) => {
17
+ const [firstValue] = values;
18
+ const raw = firstValue?.raw || strings.raw?.join("") || strings.join("");
19
+ let cwd = cwdBase;
20
+
21
+ return {
22
+ cwd(nextCwd) {
23
+ cwd = nextCwd;
24
+ return this;
25
+ },
26
+ quiet() {
27
+ return this;
28
+ },
29
+ async nothrow() {
30
+ try {
31
+ const result = await execFileAsync("sh", ["-lc", raw], { cwd });
32
+ return {
33
+ text() {
34
+ return result.stdout;
35
+ },
36
+ stderr: Buffer.from(result.stderr),
37
+ exitCode: 0,
38
+ };
39
+ } catch (error) {
40
+ return {
41
+ text() {
42
+ return error.stdout || "";
43
+ },
44
+ stderr: Buffer.from(error.stderr || ""),
45
+ exitCode: typeof error.code === "number" ? error.code : 1,
46
+ };
47
+ }
48
+ },
49
+ };
50
+ };
51
+
52
+ shell.escape = shellEscape;
53
+ return shell;
54
+ }
55
+
56
+ function printUsage() {
57
+ process.stdout.write(
58
+ [
59
+ "Usage:",
60
+ " opencode-worktree-workflow wt-new <title> [--json]",
61
+ " opencode-worktree-workflow wt-clean [preview|apply] [selectors...] [--json]",
62
+ "",
63
+ "Examples:",
64
+ " opencode-worktree-workflow wt-new \"Improve checkout retry logic\"",
65
+ " opencode-worktree-workflow wt-clean preview",
66
+ " opencode-worktree-workflow wt-clean apply feature/foo",
67
+ ].join("\n") + "\n",
68
+ );
69
+ }
70
+
71
+ export function parseCliArgs(argv) {
72
+ const outputJson = argv.includes("--json");
73
+ const args = argv.filter((arg) => arg !== "--json");
74
+ return { outputJson, args };
75
+ }
76
+
77
+ export async function run(argv = process.argv.slice(2)) {
78
+ const { outputJson, args } = parseCliArgs(argv);
79
+ const [command, ...rest] = args;
80
+
81
+ if (!command || command === "--help" || command === "-h" || command === "help") {
82
+ printUsage();
83
+ process.exitCode = command ? 0 : 1;
84
+ return;
85
+ }
86
+
87
+ const plugin = await WorktreeWorkflowPlugin({
88
+ $: createShell(process.cwd()),
89
+ directory: process.cwd(),
90
+ });
91
+
92
+ let result;
93
+
94
+ if (command === "wt-new") {
95
+ const title = rest.join(" ").trim();
96
+
97
+ if (!title) {
98
+ throw new Error("wt-new requires a descriptive title.");
99
+ }
100
+
101
+ result = await plugin.tool.worktree_prepare.execute(
102
+ { title },
103
+ { metadata() {}, worktree: process.cwd() },
104
+ );
105
+ } else if (command === "wt-clean") {
106
+ const raw = rest.join(" ").trim();
107
+ result = await plugin.tool.worktree_cleanup.execute(
108
+ { raw, selectors: [] },
109
+ { metadata() {}, worktree: process.cwd() },
110
+ );
111
+ } else {
112
+ throw new Error(`Unknown command: ${command}`);
113
+ }
114
+
115
+ if (outputJson) {
116
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
117
+ return;
118
+ }
119
+
120
+ process.stdout.write(`${result.message || JSON.stringify(result, null, 2)}\n`);
121
+ }
122
+
123
+ const invokedAsScript = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
124
+
125
+ if (invokedAsScript) {
126
+ run().catch((error) => {
127
+ process.stderr.write(`${error.message || String(error)}\n`);
128
+ process.exitCode = 1;
129
+ });
130
+ }
package/src/index.js CHANGED
@@ -13,6 +13,8 @@ const DEFAULTS = {
13
13
  protectedBranches: [],
14
14
  };
15
15
 
16
+ const RESULT_SCHEMA_VERSION = "1.0.0";
17
+
16
18
  async function pathExists(targetPath) {
17
19
  try {
18
20
  await fs.access(targetPath);
@@ -163,6 +165,18 @@ function formatPreview(grouped, defaultBranch) {
163
165
  ].join("\n");
164
166
  }
165
167
 
168
+ function formatPrepareSummary(result) {
169
+ return [
170
+ `Created worktree for "${result.title}".`,
171
+ `- branch: ${result.branch}`,
172
+ `- worktree: ${result.worktree_path}`,
173
+ `- default branch: ${result.default_branch}`,
174
+ `- base branch: ${result.base_branch}`,
175
+ `- base ref: ${result.base_ref}`,
176
+ `- base commit: ${result.base_commit}`,
177
+ ].join("\n");
178
+ }
179
+
166
180
  function formatCleanupSummary(defaultBranch, removed, failed, requestedSelectors) {
167
181
  const lines = [`Cleaned worktrees relative to ${defaultBranch}:`];
168
182
 
@@ -194,6 +208,88 @@ function formatCleanupSummary(defaultBranch, removed, failed, requestedSelectors
194
208
  return lines.join("\n");
195
209
  }
196
210
 
211
+ function toStructuredCleanupItem(item) {
212
+ return {
213
+ branch: item.branch ?? null,
214
+ worktree_path: item.path ?? item.worktree_path ?? null,
215
+ head: item.head ?? null,
216
+ status: item.status ?? null,
217
+ reason: item.reason ?? null,
218
+ detached: Boolean(item.detached),
219
+ selectable: typeof item.selectable === "boolean" ? item.selectable : null,
220
+ };
221
+ }
222
+
223
+ function toStructuredCleanupFailure(item) {
224
+ return {
225
+ selector: item.selector ?? null,
226
+ branch: item.branch ?? null,
227
+ worktree_path: item.path ?? item.worktree_path ?? null,
228
+ head: item.head ?? null,
229
+ status: item.status ?? null,
230
+ reason: item.reason ?? null,
231
+ detached: Boolean(item.detached),
232
+ selectable: typeof item.selectable === "boolean" ? item.selectable : null,
233
+ };
234
+ }
235
+
236
+ function buildPrepareResult({ title, branch, worktreePath, defaultBranch, baseBranch, baseRef, baseCommit }) {
237
+ const result = {
238
+ schema_version: RESULT_SCHEMA_VERSION,
239
+ ok: true,
240
+ title,
241
+ branch,
242
+ worktree_path: worktreePath,
243
+ default_branch: defaultBranch,
244
+ base_branch: baseBranch,
245
+ base_ref: baseRef,
246
+ base_commit: baseCommit,
247
+ created: true,
248
+ };
249
+
250
+ return {
251
+ ...result,
252
+ message: formatPrepareSummary(result),
253
+ };
254
+ }
255
+
256
+ function buildCleanupPreviewResult({ defaultBranch, baseBranch, baseRef, grouped }) {
257
+ const structuredGroups = {
258
+ safe: grouped.safe.map(toStructuredCleanupItem),
259
+ review: grouped.review.map(toStructuredCleanupItem),
260
+ blocked: grouped.blocked.map(toStructuredCleanupItem),
261
+ };
262
+
263
+ return {
264
+ schema_version: RESULT_SCHEMA_VERSION,
265
+ ok: true,
266
+ mode: "preview",
267
+ default_branch: defaultBranch,
268
+ base_branch: baseBranch,
269
+ base_ref: baseRef,
270
+ groups: structuredGroups,
271
+ message: formatPreview(grouped, baseBranch),
272
+ };
273
+ }
274
+
275
+ function buildCleanupApplyResult({ defaultBranch, baseBranch, baseRef, removed, failed, requestedSelectors }) {
276
+ return {
277
+ schema_version: RESULT_SCHEMA_VERSION,
278
+ ok: true,
279
+ mode: "apply",
280
+ default_branch: defaultBranch,
281
+ base_branch: baseBranch,
282
+ base_ref: baseRef,
283
+ requested_selectors: requestedSelectors,
284
+ removed: removed.map((item) => ({
285
+ ...toStructuredCleanupItem(item),
286
+ selected: Boolean(item.selected),
287
+ })),
288
+ failed: failed.map(toStructuredCleanupFailure),
289
+ message: formatCleanupSummary(baseBranch, removed, failed, requestedSelectors),
290
+ };
291
+ }
292
+
197
293
  function splitCleanupToken(value) {
198
294
  if (typeof value !== "string") {
199
295
  return [];
@@ -275,8 +371,15 @@ function normalizeCleanupArgs(args, config) {
275
371
  }
276
372
 
277
373
  export const __internal = {
374
+ RESULT_SCHEMA_VERSION,
375
+ buildCleanupApplyResult,
376
+ buildCleanupPreviewResult,
377
+ buildPrepareResult,
378
+ classifyEntry,
278
379
  parseCleanupRawArguments,
279
380
  normalizeCleanupArgs,
381
+ toStructuredCleanupFailure,
382
+ toStructuredCleanupItem,
280
383
  };
281
384
 
282
385
  function selectorMatches(item, selector) {
@@ -465,6 +568,18 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
465
568
  return remoteExists.exitCode === 0 ? `${remote}/${baseBranch}` : baseBranch;
466
569
  }
467
570
 
571
+ async function resolveBaseTarget(repoRoot, config) {
572
+ const defaultBranch = await getDefaultBranch(repoRoot, config.remote);
573
+ const baseBranch = await resolveBaseBranch(repoRoot, config.remote, config.baseBranch);
574
+ const baseRef = await getBaseRef(repoRoot, config.remote, baseBranch);
575
+
576
+ return {
577
+ defaultBranch,
578
+ baseBranch,
579
+ baseRef,
580
+ };
581
+ }
582
+
468
583
  async function ensureBranchDoesNotExist(repoRoot, branchName) {
469
584
  const exists = await git(["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], {
470
585
  cwd: repoRoot,
@@ -488,9 +603,7 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
488
603
 
489
604
  const repoRoot = await getRepoRoot();
490
605
  const config = await loadWorkflowConfig(repoRoot);
491
- const defaultBranch = await getDefaultBranch(repoRoot, config.remote);
492
- const baseBranch = await resolveBaseBranch(repoRoot, config.remote, config.baseBranch);
493
- const baseRef = await getBaseRef(repoRoot, config.remote, baseBranch);
606
+ const { defaultBranch, baseBranch, baseRef } = await resolveBaseTarget(repoRoot, config);
494
607
  const baseCommit = (await git(["rev-parse", baseRef], { cwd: repoRoot })).stdout;
495
608
  const slug = slugifyTitle(args.title);
496
609
 
@@ -519,15 +632,15 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
519
632
  );
520
633
  }
521
634
 
522
- return [
523
- `Created worktree for \"${args.title}\".`,
524
- `- branch: ${branchName}`,
525
- `- worktree: ${worktreePath}`,
526
- `- default branch: ${defaultBranch}`,
527
- `- base branch: ${baseBranch}`,
528
- `- base ref: ${baseRef}`,
529
- `- base commit: ${baseCommit}`,
530
- ].join("\n");
635
+ return buildPrepareResult({
636
+ title: args.title,
637
+ branch: branchName,
638
+ worktreePath,
639
+ defaultBranch,
640
+ baseBranch,
641
+ baseRef,
642
+ baseCommit,
643
+ });
531
644
  },
532
645
  }),
533
646
  worktree_cleanup: tool({
@@ -547,9 +660,7 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
547
660
 
548
661
  context.metadata({ title: `Clean worktrees (${normalizedArgs.mode})` });
549
662
 
550
- const defaultBranch = await getDefaultBranch(repoRoot, config.remote);
551
- const baseBranch = await resolveBaseBranch(repoRoot, config.remote, config.baseBranch);
552
- const baseRef = await getBaseRef(repoRoot, config.remote, baseBranch);
663
+ const { defaultBranch, baseBranch, baseRef } = await resolveBaseTarget(repoRoot, config);
553
664
  const activeWorktree = path.resolve(context.worktree || repoRoot);
554
665
  const worktreeList = await git(["worktree", "list", "--porcelain"], { cwd: repoRoot });
555
666
  const entries = parseWorktreeList(worktreeList.stdout);
@@ -585,7 +696,12 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
585
696
  }
586
697
 
587
698
  if (normalizedArgs.mode !== "apply") {
588
- return formatPreview(grouped, baseBranch);
699
+ return buildCleanupPreviewResult({
700
+ defaultBranch,
701
+ baseBranch,
702
+ baseRef,
703
+ grouped,
704
+ });
589
705
  }
590
706
 
591
707
  const requestedSelectors = [...new Set(normalizedArgs.selectors || [])];
@@ -614,7 +730,10 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
614
730
  continue;
615
731
  }
616
732
 
617
- selected.push(match);
733
+ selected.push({
734
+ ...match,
735
+ selector,
736
+ });
618
737
  }
619
738
 
620
739
  const targets = [...grouped.safe];
@@ -623,6 +742,7 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
623
742
  if (!targets.some((target) => target.path === item.path)) {
624
743
  targets.push({
625
744
  ...item,
745
+ selector: item.selector ?? null,
626
746
  selected: true,
627
747
  });
628
748
  }
@@ -668,7 +788,14 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
668
788
  allowFailure: true,
669
789
  });
670
790
 
671
- return formatCleanupSummary(baseBranch, removed, failed, requestedSelectors);
791
+ return buildCleanupApplyResult({
792
+ defaultBranch,
793
+ baseBranch,
794
+ baseRef,
795
+ removed,
796
+ failed,
797
+ requestedSelectors,
798
+ });
672
799
  },
673
800
  }),
674
801
  },