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.
@@ -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.21.4.tar.gz"
5
- sha256 "7d21ed495bfb2b737f6a519dc9bfcdb2ef341636b138c6450e1848fd6819c924"
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:
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-pilot",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "type": "module",
5
5
  "main": "plugin/index.js",
6
6
  "description": "Automation daemon for OpenCode - polls for work and spawns sessions",
@@ -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: