opencode-pilot 0.22.0 → 0.23.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/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
|
@@ -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.22.0.tar.gz"
|
|
5
|
+
sha256 "b4437190cab6bff8c03ab93b6d70c8de7ccb3e396decc36b460d53f3ee482dde"
|
|
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:
|