opencode-pilot 0.22.0 → 0.23.1
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/.devcontainer/devcontainer.json +1 -1
- package/.github/workflows/ci.yml +1 -1
- package/CONTRIBUTING.md +43 -54
- package/Formula/opencode-pilot.rb +2 -2
- package/README.md +19 -1
- package/examples/config.yaml +6 -0
- package/package.json +1 -1
- package/test/unit/actions.test.js +98 -0
- package/test/unit/poll-service.test.js +34 -0
- package/test/unit/repo-config.test.js +39 -0
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"ghcr.io/devcontainers/features/git:1": {},
|
|
6
6
|
"ghcr.io/devcontainers/features/github-cli:1": {}
|
|
7
7
|
},
|
|
8
|
-
"postCreateCommand": "sudo apt-get update && sudo apt-get install -y
|
|
8
|
+
"postCreateCommand": "sudo apt-get update && sudo apt-get install -y ripgrep && npm install -g opencode-ai@latest",
|
|
9
9
|
"customizations": {
|
|
10
10
|
"vscode": {
|
|
11
11
|
"extensions": [
|
package/.github/workflows/ci.yml
CHANGED
package/CONTRIBUTING.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Contributing to opencode-
|
|
1
|
+
# Contributing to opencode-pilot
|
|
2
2
|
|
|
3
3
|
Thanks for your interest in contributing!
|
|
4
4
|
|
|
@@ -6,84 +6,75 @@ Thanks for your interest in contributing!
|
|
|
6
6
|
|
|
7
7
|
1. Clone the repository:
|
|
8
8
|
```bash
|
|
9
|
-
git clone https://github.com/athal7/opencode-
|
|
10
|
-
cd opencode-
|
|
9
|
+
git clone https://github.com/athal7/opencode-pilot.git
|
|
10
|
+
cd opencode-pilot
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
2. Install
|
|
13
|
+
2. Install dependencies:
|
|
14
14
|
```bash
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
3. Set required environment variables:
|
|
19
|
-
```bash
|
|
20
|
-
export NTFY_TOPIC=your-test-topic
|
|
15
|
+
npm install
|
|
21
16
|
```
|
|
22
17
|
|
|
23
18
|
## Running Tests
|
|
24
19
|
|
|
25
|
-
Run the full test suite:
|
|
26
20
|
```bash
|
|
27
|
-
|
|
21
|
+
npm test # Unit tests
|
|
22
|
+
npm run test:integration # Integration tests
|
|
23
|
+
npm run test:all # All tests
|
|
28
24
|
```
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
- **File structure tests** - Verify all plugin files exist
|
|
32
|
-
- **Syntax validation** - Run `node --check` on all JS files
|
|
33
|
-
- **Export structure tests** - Verify expected functions are exported
|
|
34
|
-
- **Integration tests** - Test plugin loads in OpenCode without hanging (requires opencode CLI)
|
|
26
|
+
Tests use the Node.js built-in test runner (`node:test`) with `node:assert`.
|
|
35
27
|
|
|
36
28
|
## Writing Tests
|
|
37
29
|
|
|
38
|
-
Tests live in `test/`
|
|
30
|
+
Tests live in `test/unit/` and `test/integration/`. Each test file follows this pattern:
|
|
39
31
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# Use assertions from test_helper.bash
|
|
44
|
-
assert_file_exists "$PLUGIN_DIR/myfile.js"
|
|
45
|
-
assert_contains "$output" "expected string"
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
# Register and run
|
|
49
|
-
run_test "my_feature" "test_my_feature"
|
|
50
|
-
```
|
|
32
|
+
```js
|
|
33
|
+
import { test, describe } from 'node:test';
|
|
34
|
+
import assert from 'node:assert';
|
|
51
35
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (myFunction() !== 'expected') throw new Error('Failed');
|
|
58
|
-
console.log('PASS');
|
|
59
|
-
" || return 1
|
|
60
|
-
}
|
|
36
|
+
describe('myModule', () => {
|
|
37
|
+
test('does the thing', () => {
|
|
38
|
+
assert.strictEqual(actual, expected);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
61
41
|
```
|
|
62
42
|
|
|
63
43
|
## Code Style
|
|
64
44
|
|
|
65
45
|
- Use ES modules (`import`/`export`)
|
|
66
46
|
- Use `async`/`await` for async operations
|
|
67
|
-
- Log with `[opencode-
|
|
47
|
+
- Log with `[opencode-pilot]` prefix
|
|
68
48
|
- Handle errors gracefully (log, don't crash OpenCode)
|
|
69
|
-
- No external dependencies
|
|
49
|
+
- No external dependencies beyond what's in `package.json`
|
|
70
50
|
|
|
71
|
-
##
|
|
51
|
+
## Project Architecture
|
|
72
52
|
|
|
73
53
|
```
|
|
74
54
|
plugin/
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
├──
|
|
78
|
-
├──
|
|
79
|
-
|
|
55
|
+
└── index.js # OpenCode plugin entry point (auto-starts daemon)
|
|
56
|
+
service/
|
|
57
|
+
├── server.js # HTTP server and polling orchestration
|
|
58
|
+
├── poll-service.js # Polling lifecycle management
|
|
59
|
+
├── poller.js # MCP tool polling
|
|
60
|
+
├── actions.js # Session creation and template expansion
|
|
61
|
+
├── readiness.js # Evaluate item readiness (labels, deps, priority)
|
|
62
|
+
├── worktree.js # Git worktree management
|
|
63
|
+
├── repo-config.js # Repository discovery and config
|
|
64
|
+
├── logger.js # Debug logging
|
|
65
|
+
├── utils.js # Shared utilities
|
|
66
|
+
├── version.js # Package version detection
|
|
67
|
+
└── presets/
|
|
68
|
+
├── index.js # Preset loader
|
|
69
|
+
├── github.yaml # GitHub source presets
|
|
70
|
+
└── linear.yaml # Linear source presets
|
|
80
71
|
```
|
|
81
72
|
|
|
82
73
|
## Submitting Changes
|
|
83
74
|
|
|
84
75
|
1. Create a feature branch: `git checkout -b my-feature`
|
|
85
76
|
2. Make your changes
|
|
86
|
-
3. Run tests:
|
|
77
|
+
3. Run tests: `npm test`
|
|
87
78
|
4. Commit with a clear message following conventional commits:
|
|
88
79
|
- `feat(#1): add idle notifications`
|
|
89
80
|
- `fix(#3): handle network timeout`
|
|
@@ -91,12 +82,10 @@ plugin/
|
|
|
91
82
|
|
|
92
83
|
## Releasing
|
|
93
84
|
|
|
94
|
-
Releases are automated via
|
|
95
|
-
|
|
96
|
-
1. Tag the commit: `git tag v1.0.0`
|
|
97
|
-
2. Push the tag: `git push origin v1.0.0`
|
|
85
|
+
Releases are automated via [semantic-release](https://github.com/semantic-release/semantic-release) on merge to `main`. The CI pipeline will:
|
|
98
86
|
|
|
99
|
-
The release workflow will:
|
|
100
87
|
1. Run tests
|
|
101
|
-
2.
|
|
102
|
-
3.
|
|
88
|
+
2. Determine the next version from commit messages
|
|
89
|
+
3. Publish to npm
|
|
90
|
+
4. Create a GitHub release
|
|
91
|
+
5. Update the Homebrew formula
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
class OpencodePilot < Formula
|
|
2
2
|
desc "Automation daemon for OpenCode - polls GitHub/Linear issues and spawns sessions"
|
|
3
3
|
homepage "https://github.com/athal7/opencode-pilot"
|
|
4
|
-
url "https://github.com/athal7/opencode-pilot/archive/refs/tags/v0.
|
|
5
|
-
sha256 "
|
|
4
|
+
url "https://github.com/athal7/opencode-pilot/archive/refs/tags/v0.23.0.tar.gz"
|
|
5
|
+
sha256 "1d245b1ea0ec5db353b5558fe2dbb59de727983775464ea61d807cdbd8061087"
|
|
6
6
|
license "MIT"
|
|
7
7
|
|
|
8
8
|
depends_on "node"
|
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ See [examples/config.yaml](examples/config.yaml) for a complete example with all
|
|
|
57
57
|
- **`server_port`** - Preferred OpenCode server port (e.g., `4096`). When multiple OpenCode instances are running, pilot attaches sessions to this port.
|
|
58
58
|
- **`startup_delay`** - Milliseconds to wait before first poll (default: `10000`). Allows OpenCode server time to fully initialize after restart.
|
|
59
59
|
- **`repos_dir`** - Directory containing git repos (e.g., `~/code`). Pilot auto-discovers repos by scanning git remotes (both `origin` and `upstream` for fork support).
|
|
60
|
-
- **`defaults`** - Default values applied to all sources
|
|
60
|
+
- **`defaults`** - Default values applied to all sources (`agent`, `model`, `prompt`, etc.)
|
|
61
61
|
- **`sources`** - What to poll (presets, shorthand, or full config)
|
|
62
62
|
- **`tools`** - Field mappings to normalize different MCP APIs
|
|
63
63
|
- **`repos`** - Explicit repository paths (overrides auto-discovery from `repos_dir`)
|
|
@@ -83,6 +83,24 @@ Session names for `my-prs-attention` indicate the condition: "Conflicts: {title}
|
|
|
83
83
|
|
|
84
84
|
Create prompt templates as markdown files in `~/.config/opencode/pilot/templates/`. Templates support placeholders like `{title}`, `{body}`, `{number}`, `{html_url}`, etc.
|
|
85
85
|
|
|
86
|
+
### Model Selection
|
|
87
|
+
|
|
88
|
+
Override the default model used by the agent for pilot sessions. This avoids creating a separate agent just to use a different model.
|
|
89
|
+
|
|
90
|
+
```yaml
|
|
91
|
+
defaults:
|
|
92
|
+
agent: plan
|
|
93
|
+
model: anthropic/claude-sonnet-4-20250514 # Applied to all sources
|
|
94
|
+
|
|
95
|
+
sources:
|
|
96
|
+
- preset: github/review-requests
|
|
97
|
+
model: anthropic/claude-haiku-3.5 # Override for this source only
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Format: `provider/model-id` (e.g., `anthropic/claude-sonnet-4-20250514`). If no provider prefix, defaults to `anthropic`.
|
|
101
|
+
|
|
102
|
+
Priority: source `model` > defaults `model` > agent's built-in default.
|
|
103
|
+
|
|
86
104
|
### Session and Sandbox Reuse
|
|
87
105
|
|
|
88
106
|
By default, pilot reuses existing sessions and sandboxes to avoid duplicates:
|
package/examples/config.yaml
CHANGED
|
@@ -19,6 +19,10 @@ repos_dir: ~/code
|
|
|
19
19
|
defaults:
|
|
20
20
|
agent: plan
|
|
21
21
|
prompt: default
|
|
22
|
+
# Model selection: override the agent's default model
|
|
23
|
+
# Format: provider/model-id (e.g., anthropic/claude-sonnet-4-20250514)
|
|
24
|
+
# If no provider prefix, defaults to "anthropic"
|
|
25
|
+
# model: anthropic/claude-sonnet-4-20250514
|
|
22
26
|
# Session reuse: append to existing non-archived session instead of creating new
|
|
23
27
|
# Default: true. Set to false to always create new sessions.
|
|
24
28
|
# reuse_active_session: true
|
|
@@ -32,6 +36,8 @@ sources:
|
|
|
32
36
|
prompt: worktree
|
|
33
37
|
|
|
34
38
|
- preset: github/review-requests
|
|
39
|
+
# Per-source model override (takes precedence over defaults.model)
|
|
40
|
+
# model: anthropic/claude-haiku-3.5
|
|
35
41
|
|
|
36
42
|
# PRs needing attention (conflicts OR human feedback)
|
|
37
43
|
# Session names dynamically indicate the condition: "Conflicts: ...", "Feedback: ...", or "Conflicts+Feedback: ..."
|
package/package.json
CHANGED
|
@@ -248,6 +248,36 @@ Check for bugs and security issues.`;
|
|
|
248
248
|
assert.strictEqual(config.working_dir, '~/workspaces');
|
|
249
249
|
});
|
|
250
250
|
|
|
251
|
+
test('defaults model is used when source and repo have no model', async () => {
|
|
252
|
+
const { getActionConfig } = await import('../../service/actions.js');
|
|
253
|
+
|
|
254
|
+
const source = { name: 'my-issues' };
|
|
255
|
+
const repoConfig = { path: '~/code/backend' };
|
|
256
|
+
const defaults = { model: 'anthropic/claude-haiku-3.5' };
|
|
257
|
+
|
|
258
|
+
const config = getActionConfig(source, repoConfig, defaults);
|
|
259
|
+
|
|
260
|
+
assert.strictEqual(config.model, 'anthropic/claude-haiku-3.5');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('source model overrides defaults and repo model', async () => {
|
|
264
|
+
const { getActionConfig } = await import('../../service/actions.js');
|
|
265
|
+
|
|
266
|
+
const source = {
|
|
267
|
+
name: 'my-issues',
|
|
268
|
+
model: 'anthropic/claude-sonnet-4-20250514'
|
|
269
|
+
};
|
|
270
|
+
const repoConfig = {
|
|
271
|
+
path: '~/code/backend',
|
|
272
|
+
model: 'anthropic/claude-haiku-3.5'
|
|
273
|
+
};
|
|
274
|
+
const defaults = { model: 'anthropic/claude-haiku-3.5' };
|
|
275
|
+
|
|
276
|
+
const config = getActionConfig(source, repoConfig, defaults);
|
|
277
|
+
|
|
278
|
+
assert.strictEqual(config.model, 'anthropic/claude-sonnet-4-20250514');
|
|
279
|
+
});
|
|
280
|
+
|
|
251
281
|
});
|
|
252
282
|
|
|
253
283
|
describe('buildCommand', () => {
|
|
@@ -1367,6 +1397,74 @@ Check for bugs and security issues.`;
|
|
|
1367
1397
|
assert.strictEqual(commandBody.agent, 'code', 'Should pass agent');
|
|
1368
1398
|
assert.strictEqual(commandBody.model, 'anthropic/claude-sonnet-4-20250514', 'Should pass model as string');
|
|
1369
1399
|
});
|
|
1400
|
+
|
|
1401
|
+
test('passes model as providerID/modelID to /message endpoint', async () => {
|
|
1402
|
+
const { sendMessageToSession } = await import('../../service/actions.js');
|
|
1403
|
+
|
|
1404
|
+
let messageBody = null;
|
|
1405
|
+
|
|
1406
|
+
const mockFetch = async (url, opts) => {
|
|
1407
|
+
const urlObj = new URL(url);
|
|
1408
|
+
|
|
1409
|
+
if (urlObj.pathname.includes('/message') && opts?.method === 'POST') {
|
|
1410
|
+
messageBody = JSON.parse(opts.body);
|
|
1411
|
+
return {
|
|
1412
|
+
ok: true,
|
|
1413
|
+
json: async () => ({ success: true }),
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
return { ok: false, text: async () => 'Not found' };
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1420
|
+
await sendMessageToSession(
|
|
1421
|
+
'http://localhost:4096',
|
|
1422
|
+
'ses_existing',
|
|
1423
|
+
'/path/to/project',
|
|
1424
|
+
'Fix the bug',
|
|
1425
|
+
{
|
|
1426
|
+
fetch: mockFetch,
|
|
1427
|
+
model: 'anthropic/claude-haiku-3.5',
|
|
1428
|
+
}
|
|
1429
|
+
);
|
|
1430
|
+
|
|
1431
|
+
assert.strictEqual(messageBody.providerID, 'anthropic', 'Should parse provider from model');
|
|
1432
|
+
assert.strictEqual(messageBody.modelID, 'claude-haiku-3.5', 'Should parse model ID');
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
test('defaults to anthropic provider when model has no slash', async () => {
|
|
1436
|
+
const { sendMessageToSession } = await import('../../service/actions.js');
|
|
1437
|
+
|
|
1438
|
+
let messageBody = null;
|
|
1439
|
+
|
|
1440
|
+
const mockFetch = async (url, opts) => {
|
|
1441
|
+
const urlObj = new URL(url);
|
|
1442
|
+
|
|
1443
|
+
if (urlObj.pathname.includes('/message') && opts?.method === 'POST') {
|
|
1444
|
+
messageBody = JSON.parse(opts.body);
|
|
1445
|
+
return {
|
|
1446
|
+
ok: true,
|
|
1447
|
+
json: async () => ({ success: true }),
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
return { ok: false, text: async () => 'Not found' };
|
|
1452
|
+
};
|
|
1453
|
+
|
|
1454
|
+
await sendMessageToSession(
|
|
1455
|
+
'http://localhost:4096',
|
|
1456
|
+
'ses_existing',
|
|
1457
|
+
'/path/to/project',
|
|
1458
|
+
'Fix the bug',
|
|
1459
|
+
{
|
|
1460
|
+
fetch: mockFetch,
|
|
1461
|
+
model: 'claude-haiku-3.5',
|
|
1462
|
+
}
|
|
1463
|
+
);
|
|
1464
|
+
|
|
1465
|
+
assert.strictEqual(messageBody.providerID, 'anthropic', 'Should default to anthropic provider');
|
|
1466
|
+
assert.strictEqual(messageBody.modelID, 'claude-haiku-3.5', 'Should use full string as model ID');
|
|
1467
|
+
});
|
|
1370
1468
|
});
|
|
1371
1469
|
|
|
1372
1470
|
describe('session reuse', () => {
|
|
@@ -204,6 +204,40 @@ sources:
|
|
|
204
204
|
assert.strictEqual(config.worktree_name, 'issue-{number}');
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
+
test('model from source overrides repoConfig model', async () => {
|
|
208
|
+
const { buildActionConfigFromSource } = await import('../../service/poll-service.js');
|
|
209
|
+
|
|
210
|
+
const source = {
|
|
211
|
+
name: 'test-source',
|
|
212
|
+
model: 'anthropic/claude-sonnet-4-20250514'
|
|
213
|
+
};
|
|
214
|
+
const repoConfig = {
|
|
215
|
+
path: '~/code/default',
|
|
216
|
+
model: 'anthropic/claude-haiku-3.5'
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const config = buildActionConfigFromSource(source, repoConfig);
|
|
220
|
+
|
|
221
|
+
assert.strictEqual(config.model, 'anthropic/claude-sonnet-4-20250514');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('falls back to repoConfig model when source has none', async () => {
|
|
225
|
+
const { buildActionConfigFromSource } = await import('../../service/poll-service.js');
|
|
226
|
+
|
|
227
|
+
const source = {
|
|
228
|
+
name: 'test-source'
|
|
229
|
+
// No model
|
|
230
|
+
};
|
|
231
|
+
const repoConfig = {
|
|
232
|
+
path: '~/code/default',
|
|
233
|
+
model: 'anthropic/claude-haiku-3.5'
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const config = buildActionConfigFromSource(source, repoConfig);
|
|
237
|
+
|
|
238
|
+
assert.strictEqual(config.model, 'anthropic/claude-haiku-3.5');
|
|
239
|
+
});
|
|
240
|
+
|
|
207
241
|
});
|
|
208
242
|
|
|
209
243
|
describe('per-item repo resolution', () => {
|
|
@@ -999,6 +999,45 @@ sources:
|
|
|
999
999
|
assert.strictEqual(sources[0].model, 'claude-3-sonnet');
|
|
1000
1000
|
});
|
|
1001
1001
|
|
|
1002
|
+
test('defaults model flows through to sources', async () => {
|
|
1003
|
+
writeFileSync(configPath, `
|
|
1004
|
+
defaults:
|
|
1005
|
+
model: anthropic/claude-haiku-3.5
|
|
1006
|
+
|
|
1007
|
+
sources:
|
|
1008
|
+
- preset: github/my-issues
|
|
1009
|
+
- preset: github/review-requests
|
|
1010
|
+
`);
|
|
1011
|
+
|
|
1012
|
+
const { loadRepoConfig, getSources } = await import('../../service/repo-config.js');
|
|
1013
|
+
loadRepoConfig(configPath);
|
|
1014
|
+
const sources = getSources();
|
|
1015
|
+
|
|
1016
|
+
// Both sources should inherit model from defaults
|
|
1017
|
+
assert.strictEqual(sources[0].model, 'anthropic/claude-haiku-3.5');
|
|
1018
|
+
assert.strictEqual(sources[1].model, 'anthropic/claude-haiku-3.5');
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
test('source model overrides defaults model', async () => {
|
|
1022
|
+
writeFileSync(configPath, `
|
|
1023
|
+
defaults:
|
|
1024
|
+
model: anthropic/claude-haiku-3.5
|
|
1025
|
+
|
|
1026
|
+
sources:
|
|
1027
|
+
- preset: github/my-issues
|
|
1028
|
+
model: anthropic/claude-sonnet-4-20250514
|
|
1029
|
+
- preset: github/review-requests
|
|
1030
|
+
`);
|
|
1031
|
+
|
|
1032
|
+
const { loadRepoConfig, getSources } = await import('../../service/repo-config.js');
|
|
1033
|
+
loadRepoConfig(configPath);
|
|
1034
|
+
const sources = getSources();
|
|
1035
|
+
|
|
1036
|
+
// First source overrides, second inherits
|
|
1037
|
+
assert.strictEqual(sources[0].model, 'anthropic/claude-sonnet-4-20250514');
|
|
1038
|
+
assert.strictEqual(sources[1].model, 'anthropic/claude-haiku-3.5');
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1002
1041
|
test('getDefaults returns defaults section', async () => {
|
|
1003
1042
|
writeFileSync(configPath, `
|
|
1004
1043
|
defaults:
|