@sven1103/opencode-worktree-workflow 0.5.1 → 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 +107 -3
- package/package.json +7 -2
- package/schemas/worktree-cleanup-apply.result.schema.json +185 -0
- package/schemas/worktree-cleanup-preview.result.schema.json +109 -0
- package/schemas/worktree-prepare.result.schema.json +64 -0
- package/skills/worktree-workflow/SKILL.md +62 -0
- package/src/cli.js +130 -0
- package/src/index.js +131 -12
package/README.md
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
# OpenCode Worktree Workflow
|
|
2
2
|
|
|
3
|
-
`@sven1103/opencode-worktree-workflow` is an
|
|
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
|
|
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.
|
|
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) {
|
|
@@ -529,15 +632,15 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
|
|
|
529
632
|
);
|
|
530
633
|
}
|
|
531
634
|
|
|
532
|
-
return
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
635
|
+
return buildPrepareResult({
|
|
636
|
+
title: args.title,
|
|
637
|
+
branch: branchName,
|
|
638
|
+
worktreePath,
|
|
639
|
+
defaultBranch,
|
|
640
|
+
baseBranch,
|
|
641
|
+
baseRef,
|
|
642
|
+
baseCommit,
|
|
643
|
+
});
|
|
541
644
|
},
|
|
542
645
|
}),
|
|
543
646
|
worktree_cleanup: tool({
|
|
@@ -593,7 +696,12 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
|
|
|
593
696
|
}
|
|
594
697
|
|
|
595
698
|
if (normalizedArgs.mode !== "apply") {
|
|
596
|
-
return
|
|
699
|
+
return buildCleanupPreviewResult({
|
|
700
|
+
defaultBranch,
|
|
701
|
+
baseBranch,
|
|
702
|
+
baseRef,
|
|
703
|
+
grouped,
|
|
704
|
+
});
|
|
597
705
|
}
|
|
598
706
|
|
|
599
707
|
const requestedSelectors = [...new Set(normalizedArgs.selectors || [])];
|
|
@@ -622,7 +730,10 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
|
|
|
622
730
|
continue;
|
|
623
731
|
}
|
|
624
732
|
|
|
625
|
-
selected.push(
|
|
733
|
+
selected.push({
|
|
734
|
+
...match,
|
|
735
|
+
selector,
|
|
736
|
+
});
|
|
626
737
|
}
|
|
627
738
|
|
|
628
739
|
const targets = [...grouped.safe];
|
|
@@ -631,6 +742,7 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
|
|
|
631
742
|
if (!targets.some((target) => target.path === item.path)) {
|
|
632
743
|
targets.push({
|
|
633
744
|
...item,
|
|
745
|
+
selector: item.selector ?? null,
|
|
634
746
|
selected: true,
|
|
635
747
|
});
|
|
636
748
|
}
|
|
@@ -676,7 +788,14 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
|
|
|
676
788
|
allowFailure: true,
|
|
677
789
|
});
|
|
678
790
|
|
|
679
|
-
return
|
|
791
|
+
return buildCleanupApplyResult({
|
|
792
|
+
defaultBranch,
|
|
793
|
+
baseBranch,
|
|
794
|
+
baseRef,
|
|
795
|
+
removed,
|
|
796
|
+
failed,
|
|
797
|
+
requestedSelectors,
|
|
798
|
+
});
|
|
680
799
|
},
|
|
681
800
|
}),
|
|
682
801
|
},
|