opencode-pilot 0.13.1 → 0.14.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/README.md CHANGED
@@ -94,17 +94,9 @@ opencode-pilot test-mapping MCP # Test field mappings
94
94
 
95
95
  ## Known Issues
96
96
 
97
- ### Sessions attached to global server run in wrong directory
97
+ None currently! Previous issues have been resolved:
98
98
 
99
- When using `server_port` to attach sessions to a global OpenCode server (e.g., OpenCode Desktop with worktree="/"), sessions are created in the server's working directory (typically home) instead of the project directory. This means:
100
-
101
- - File tools resolve paths relative to home, not the project
102
- - The agent sees the wrong `Working directory` in system prompt
103
- - Git operations may target the wrong repository
104
-
105
- **Workaround**: Don't set `server_port` in your config. Sessions will run in the correct directory but won't appear in OpenCode Desktop.
106
-
107
- **Upstream issue**: [anomalyco/opencode#7376](https://github.com/anomalyco/opencode/issues/7376)
99
+ - ~~Sessions attached to global server run in wrong directory~~ - Fixed in v0.14.0 by using the HTTP API with `?directory=` parameter instead of `opencode run --attach`
108
100
 
109
101
  ## Related
110
102
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-pilot",
3
- "version": "0.13.1",
3
+ "version": "0.14.1",
4
4
  "type": "module",
5
5
  "main": "plugin/index.js",
6
6
  "description": "Automation daemon for OpenCode - polls for work and spawns sessions",
@@ -449,6 +449,106 @@ function runSpawn(args, options = {}) {
449
449
  });
450
450
  }
451
451
 
452
+ /**
453
+ * Create a session via the OpenCode HTTP API
454
+ *
455
+ * This is a workaround for the known issue where `opencode run --attach`
456
+ * doesn't support a --dir flag, causing sessions to run in the wrong directory
457
+ * when attached to a global server.
458
+ *
459
+ * @param {string} serverUrl - Server URL (e.g., "http://localhost:4096")
460
+ * @param {string} directory - Working directory for the session
461
+ * @param {string} prompt - The prompt/message to send
462
+ * @param {object} [options] - Options
463
+ * @param {string} [options.title] - Session title
464
+ * @param {string} [options.agent] - Agent to use
465
+ * @param {string} [options.model] - Model to use
466
+ * @param {function} [options.fetch] - Custom fetch function (for testing)
467
+ * @returns {Promise<object>} Result with sessionId, success, error
468
+ */
469
+ export async function createSessionViaApi(serverUrl, directory, prompt, options = {}) {
470
+ const fetchFn = options.fetch || fetch;
471
+
472
+ try {
473
+ // Step 1: Create a new session with the directory parameter
474
+ const sessionUrl = new URL('/session', serverUrl);
475
+ sessionUrl.searchParams.set('directory', directory);
476
+
477
+ const createResponse = await fetchFn(sessionUrl.toString(), {
478
+ method: 'POST',
479
+ headers: { 'Content-Type': 'application/json' },
480
+ body: JSON.stringify({}),
481
+ });
482
+
483
+ if (!createResponse.ok) {
484
+ const errorText = await createResponse.text();
485
+ throw new Error(`Failed to create session: ${createResponse.status} ${errorText}`);
486
+ }
487
+
488
+ const session = await createResponse.json();
489
+ debug(`createSessionViaApi: created session ${session.id} in ${directory}`);
490
+
491
+ // Step 2: Update session title if provided
492
+ if (options.title) {
493
+ const updateUrl = new URL(`/session/${session.id}`, serverUrl);
494
+ updateUrl.searchParams.set('directory', directory);
495
+ await fetchFn(updateUrl.toString(), {
496
+ method: 'PATCH',
497
+ headers: { 'Content-Type': 'application/json' },
498
+ body: JSON.stringify({ title: options.title }),
499
+ });
500
+ }
501
+
502
+ // Step 3: Send the initial message
503
+ const messageUrl = new URL(`/session/${session.id}/message`, serverUrl);
504
+ messageUrl.searchParams.set('directory', directory);
505
+
506
+ // Build message body
507
+ const messageBody = {
508
+ parts: [{ type: 'text', text: prompt }],
509
+ };
510
+
511
+ // Add agent if specified
512
+ if (options.agent) {
513
+ messageBody.agent = options.agent;
514
+ }
515
+
516
+ // Add model if specified (format: provider/model)
517
+ if (options.model) {
518
+ const [providerID, modelID] = options.model.includes('/')
519
+ ? options.model.split('/', 2)
520
+ : ['anthropic', options.model];
521
+ messageBody.providerID = providerID;
522
+ messageBody.modelID = modelID;
523
+ }
524
+
525
+ const messageResponse = await fetchFn(messageUrl.toString(), {
526
+ method: 'POST',
527
+ headers: { 'Content-Type': 'application/json' },
528
+ body: JSON.stringify(messageBody),
529
+ });
530
+
531
+ if (!messageResponse.ok) {
532
+ const errorText = await messageResponse.text();
533
+ throw new Error(`Failed to send message: ${messageResponse.status} ${errorText}`);
534
+ }
535
+
536
+ debug(`createSessionViaApi: sent message to session ${session.id}`);
537
+
538
+ return {
539
+ success: true,
540
+ sessionId: session.id,
541
+ directory,
542
+ };
543
+ } catch (err) {
544
+ debug(`createSessionViaApi: error - ${err.message}`);
545
+ return {
546
+ success: false,
547
+ error: err.message,
548
+ };
549
+ }
550
+ }
551
+
452
552
  /**
453
553
  * Execute an action
454
554
  * @param {object} item - Item to create session for
@@ -456,6 +556,7 @@ function runSpawn(args, options = {}) {
456
556
  * @param {object} [options] - Execution options
457
557
  * @param {boolean} [options.dryRun] - If true, return command without executing
458
558
  * @param {function} [options.discoverServer] - Custom server discovery function (for testing)
559
+ * @param {function} [options.fetch] - Custom fetch function (for testing)
459
560
  * @returns {Promise<object>} Result with command, stdout, stderr, exitCode
460
561
  */
461
562
  export async function executeAction(item, config, options = {}) {
@@ -469,8 +570,48 @@ export async function executeAction(item, config, options = {}) {
469
570
 
470
571
  debug(`executeAction: discovered server=${serverUrl} for cwd=${cwd}`);
471
572
 
472
- // Build command info with server URL for --attach flag
473
- const cmdInfo = getCommandInfoNew(item, config, undefined, serverUrl);
573
+ // If a server is running, use the HTTP API to create the session
574
+ // This is a workaround for the known issue where --attach doesn't support --dir
575
+ // See: https://github.com/anomalyco/opencode/issues/7376
576
+ if (serverUrl) {
577
+ // Build prompt from template
578
+ const prompt = buildPromptFromTemplate(config.prompt || "default", item);
579
+
580
+ // Build session title
581
+ const sessionTitle = config.session?.name
582
+ ? buildSessionName(config.session.name, item)
583
+ : (item.title || `session-${Date.now()}`);
584
+
585
+ const apiCommand = `[API] POST ${serverUrl}/session?directory=${cwd}`;
586
+ debug(`executeAction: using HTTP API - ${apiCommand}`);
587
+
588
+ if (options.dryRun) {
589
+ return {
590
+ command: apiCommand,
591
+ dryRun: true,
592
+ method: 'api',
593
+ };
594
+ }
595
+
596
+ const result = await createSessionViaApi(serverUrl, cwd, prompt, {
597
+ title: sessionTitle,
598
+ agent: config.agent,
599
+ model: config.model,
600
+ fetch: options.fetch,
601
+ });
602
+
603
+ return {
604
+ command: apiCommand,
605
+ success: result.success,
606
+ sessionId: result.sessionId,
607
+ error: result.error,
608
+ method: 'api',
609
+ };
610
+ }
611
+
612
+ // No server running - fall back to spawning opencode run
613
+ // This works correctly because we set cwd on the spawn
614
+ const cmdInfo = getCommandInfoNew(item, config, undefined, null);
474
615
 
475
616
  // Build command string for display
476
617
  const quoteArgs = (args) => args.map(a =>
@@ -486,6 +627,7 @@ export async function executeAction(item, config, options = {}) {
486
627
  return {
487
628
  command,
488
629
  dryRun: true,
630
+ method: 'spawn',
489
631
  };
490
632
  }
491
633
 
@@ -505,6 +647,7 @@ export async function executeAction(item, config, options = {}) {
505
647
  command,
506
648
  success: true,
507
649
  pid: child.pid,
650
+ method: 'spawn',
508
651
  };
509
652
  }
510
653
 
package/service/poller.js CHANGED
@@ -255,8 +255,9 @@ async function executeCliCommand(command, args, timeout) {
255
255
  }
256
256
  return part;
257
257
  });
258
- // Quote parts with spaces
259
- cmdStr = expandedCmd.map(p => p.includes(' ') ? `"${p}"` : p).join(' ');
258
+ // Quote parts with spaces or shell special characters
259
+ const shellSpecialChars = /[ <>|&;$`"'\\!*?#~=\[\]{}()]/;
260
+ cmdStr = expandedCmd.map(p => shellSpecialChars.test(p) ? `"${p.replace(/"/g, '\\"')}"` : p).join(' ');
260
261
  } else {
261
262
  // String command - substitute ${argName} patterns
262
263
  cmdStr = command.replace(/\$\{(\w+)\}/g, (_, name) => {
@@ -1,11 +1,15 @@
1
- # GitHub source presets
1
+ # GitHub source presets (using gh CLI)
2
+ #
3
+ # These presets use the `gh` CLI instead of requiring a GitHub MCP server.
4
+ # The gh CLI must be installed and authenticated (gh auth login).
2
5
 
3
6
  # Provider-level config (applies to all GitHub presets)
4
7
  _provider:
5
- response_key: items
6
8
  mappings:
7
- # Extract repo full name from repository_url (e.g., "https://api.github.com/repos/owner/repo")
8
- repository_full_name: "repository_url:/repos\\/([^/]+\\/[^/]+)$/"
9
+ # Map gh CLI fields to standard fields
10
+ html_url: url
11
+ repository_full_name: repository.nameWithOwner
12
+ updated_at: updatedAt
9
13
  # Reprocess items when state changes (e.g., reopened issues)
10
14
  # Note: updated_at is NOT included because our own changes would trigger reprocessing
11
15
  reprocess_on:
@@ -15,48 +19,35 @@ _provider:
15
19
  my-issues:
16
20
  name: my-issues
17
21
  tool:
18
- mcp: github
19
- name: search_issues
20
- args:
21
- q: "is:issue assignee:@me state:open"
22
+ command: ["gh", "search", "issues", "--assignee=@me", "--state=open", "--json", "number,title,url,repository,state,body,updatedAt"]
22
23
  item:
23
- id: "{html_url}"
24
- repo: "{repository_full_name}"
24
+ id: "{url}"
25
+ repo: "{repository.nameWithOwner}"
25
26
  session:
26
27
  name: "{title}"
27
28
 
28
29
  review-requests:
29
30
  name: review-requests
30
31
  tool:
31
- mcp: github
32
- name: search_issues
33
- args:
34
- q: "is:pr review-requested:@me state:open"
32
+ command: ["gh", "search", "prs", "--review-requested=@me", "--state=open", "--json", "number,title,url,repository,state,body,updatedAt"]
35
33
  item:
36
- id: "{html_url}"
37
- repo: "{repository_full_name}"
34
+ id: "{url}"
35
+ repo: "{repository.nameWithOwner}"
38
36
  session:
39
37
  name: "Review: {title}"
40
38
 
41
39
  my-prs-feedback:
42
40
  name: my-prs-feedback
43
41
  tool:
44
- mcp: github
45
- name: search_issues
46
- args:
47
- # Catches PRs with any review activity (comments, reviews, or changes requested)
48
- # Note: comments:>0 includes both review comments and issue comments
49
- q: "is:pr author:@me state:open comments:>0"
42
+ # comments:>0 filter ensures only PRs with feedback are returned
43
+ command: ["gh", "search", "prs", "--author=@me", "--state=open", "comments:>0", "--json", "number,title,url,repository,state,body,updatedAt"]
50
44
  item:
51
- id: "{html_url}"
52
- repo: "{repository_full_name}"
45
+ id: "{url}"
46
+ repo: "{repository.nameWithOwner}"
53
47
  session:
54
48
  name: "Feedback: {title}"
55
49
  # Reprocess when PR is updated (new commits pushed, new comments, etc.)
56
50
  # This ensures we re-trigger after addressing review feedback
57
51
  reprocess_on:
58
52
  - state
59
- - updated_at
60
- # Filter out PRs where all comments are from bots or the PR author
61
- # Fetches comments via API and enriches items with _comments for readiness check
62
- filter_bot_comments: true
53
+ - updatedAt
@@ -525,7 +525,7 @@ describe('actions.js', () => {
525
525
  });
526
526
 
527
527
  describe('executeAction', () => {
528
- test('discovers server and includes --attach in dry run', async () => {
528
+ test('uses HTTP API when server is discovered (dry run)', async () => {
529
529
  const { executeAction } = await import('../../service/actions.js');
530
530
 
531
531
  const item = { number: 123, title: 'Fix bug' };
@@ -543,11 +543,13 @@ describe('actions.js', () => {
543
543
  });
544
544
 
545
545
  assert.ok(result.dryRun);
546
- assert.ok(result.command.includes('--attach'), 'Command should include --attach flag');
546
+ assert.strictEqual(result.method, 'api', 'Should use API method when server found');
547
+ assert.ok(result.command.includes('POST'), 'Command should show POST request');
547
548
  assert.ok(result.command.includes('http://localhost:4096'), 'Command should include server URL');
549
+ assert.ok(result.command.includes('directory='), 'Command should include directory param');
548
550
  });
549
551
 
550
- test('does not include --attach when no server discovered', async () => {
552
+ test('falls back to spawn when no server discovered (dry run)', async () => {
551
553
  const { executeAction } = await import('../../service/actions.js');
552
554
 
553
555
  const item = { number: 123, title: 'Fix bug' };
@@ -565,7 +567,130 @@ describe('actions.js', () => {
565
567
  });
566
568
 
567
569
  assert.ok(result.dryRun);
570
+ assert.strictEqual(result.method, 'spawn', 'Should use spawn method when no server');
568
571
  assert.ok(!result.command.includes('--attach'), 'Command should not include --attach flag');
572
+ assert.ok(result.command.includes('opencode run'), 'Command should include opencode run');
573
+ });
574
+ });
575
+
576
+ describe('createSessionViaApi', () => {
577
+ test('creates session and sends message with directory param', async () => {
578
+ const { createSessionViaApi } = await import('../../service/actions.js');
579
+
580
+ const mockSessionId = 'ses_test123';
581
+ let createCalled = false;
582
+ let messageCalled = false;
583
+ let createUrl = null;
584
+ let messageUrl = null;
585
+
586
+ const mockFetch = async (url, opts) => {
587
+ const urlObj = new URL(url);
588
+
589
+ if (urlObj.pathname === '/session' && opts?.method === 'POST') {
590
+ createCalled = true;
591
+ createUrl = url;
592
+ return {
593
+ ok: true,
594
+ json: async () => ({ id: mockSessionId }),
595
+ };
596
+ }
597
+
598
+ if (urlObj.pathname.includes('/message') && opts?.method === 'POST') {
599
+ messageCalled = true;
600
+ messageUrl = url;
601
+ return {
602
+ ok: true,
603
+ json: async () => ({ success: true }),
604
+ };
605
+ }
606
+
607
+ return { ok: false, text: async () => 'Not found' };
608
+ };
609
+
610
+ const result = await createSessionViaApi(
611
+ 'http://localhost:4096',
612
+ '/path/to/project',
613
+ 'Fix the bug',
614
+ { fetch: mockFetch }
615
+ );
616
+
617
+ assert.ok(result.success, 'Should succeed');
618
+ assert.strictEqual(result.sessionId, mockSessionId, 'Should return session ID');
619
+ assert.ok(createCalled, 'Should call create session endpoint');
620
+ assert.ok(messageCalled, 'Should call message endpoint');
621
+ // URL encodes slashes as %2F
622
+ assert.ok(createUrl.includes('directory='), 'Create URL should include directory param');
623
+ assert.ok(createUrl.includes('%2Fpath%2Fto%2Fproject'), 'Create URL should include encoded directory path');
624
+ assert.ok(messageUrl.includes('directory='), 'Message URL should include directory param');
625
+ assert.ok(messageUrl.includes('%2Fpath%2Fto%2Fproject'), 'Message URL should include encoded directory path');
626
+ });
627
+
628
+ test('handles session creation failure', async () => {
629
+ const { createSessionViaApi } = await import('../../service/actions.js');
630
+
631
+ const mockFetch = async () => ({
632
+ ok: false,
633
+ status: 500,
634
+ text: async () => 'Internal server error',
635
+ });
636
+
637
+ const result = await createSessionViaApi(
638
+ 'http://localhost:4096',
639
+ '/path/to/project',
640
+ 'Fix the bug',
641
+ { fetch: mockFetch }
642
+ );
643
+
644
+ assert.ok(!result.success, 'Should fail');
645
+ assert.ok(result.error.includes('Failed to create session'), 'Should include error message');
646
+ });
647
+
648
+ test('passes agent and model options', async () => {
649
+ const { createSessionViaApi } = await import('../../service/actions.js');
650
+
651
+ let messageBody = null;
652
+
653
+ const mockFetch = async (url, opts) => {
654
+ const urlObj = new URL(url);
655
+
656
+ if (urlObj.pathname === '/session' && opts?.method === 'POST') {
657
+ return {
658
+ ok: true,
659
+ json: async () => ({ id: 'ses_test' }),
660
+ };
661
+ }
662
+
663
+ if (urlObj.pathname.includes('/message')) {
664
+ messageBody = JSON.parse(opts.body);
665
+ return {
666
+ ok: true,
667
+ json: async () => ({ success: true }),
668
+ };
669
+ }
670
+
671
+ // PATCH for title update
672
+ if (opts?.method === 'PATCH') {
673
+ return { ok: true, json: async () => ({}) };
674
+ }
675
+
676
+ return { ok: false, text: async () => 'Not found' };
677
+ };
678
+
679
+ await createSessionViaApi(
680
+ 'http://localhost:4096',
681
+ '/path/to/project',
682
+ 'Fix the bug',
683
+ {
684
+ fetch: mockFetch,
685
+ agent: 'code',
686
+ model: 'anthropic/claude-sonnet-4-20250514',
687
+ title: 'Test Session',
688
+ }
689
+ );
690
+
691
+ assert.strictEqual(messageBody.agent, 'code', 'Should pass agent');
692
+ assert.strictEqual(messageBody.providerID, 'anthropic', 'Should parse provider from model');
693
+ assert.strictEqual(messageBody.modelID, 'claude-sonnet-4-20250514', 'Should parse model ID');
569
694
  });
570
695
  });
571
696
  });
@@ -159,15 +159,15 @@ sources:
159
159
  loadRepoConfig(configPath);
160
160
 
161
161
  const source = getSources()[0];
162
- // Item with repository_full_name (mapped from repository_url by GitHub provider)
162
+ // Item with repository.nameWithOwner (gh CLI output format)
163
163
  const item = {
164
- repository_full_name: 'myorg/backend',
164
+ repository: { nameWithOwner: 'myorg/backend' },
165
165
  number: 123,
166
- html_url: 'https://github.com/myorg/backend/issues/123'
166
+ url: 'https://github.com/myorg/backend/issues/123'
167
167
  };
168
168
 
169
- // Source should have repo field from preset (uses mapped field)
170
- assert.strictEqual(source.repo, '{repository_full_name}');
169
+ // Source should have repo field from preset (uses gh CLI field)
170
+ assert.strictEqual(source.repo, '{repository.nameWithOwner}');
171
171
 
172
172
  // resolveRepoForItem should extract repo key from item
173
173
  const repoKeys = resolveRepoForItem(source, item);
@@ -195,9 +195,9 @@ sources:
195
195
  loadRepoConfig(configPath);
196
196
 
197
197
  const source = getSources()[0];
198
- // Item with repository_full_name (mapped from repository_url by GitHub provider)
198
+ // Item with repository.nameWithOwner (gh CLI output format)
199
199
  const item = {
200
- repository_full_name: 'unknown/repo',
200
+ repository: { nameWithOwner: 'unknown/repo' },
201
201
  number: 456
202
202
  };
203
203
 
@@ -340,7 +340,7 @@ sources: []
340
340
  tools:
341
341
  github:
342
342
  mappings:
343
- url: html_url
343
+ custom_field: some_source
344
344
 
345
345
  sources: []
346
346
  `);
@@ -350,10 +350,11 @@ sources: []
350
350
 
351
351
  const toolConfig = getToolProviderConfig('github');
352
352
 
353
- // GitHub preset has response_key: items, user config doesn't override it
354
- assert.strictEqual(toolConfig.response_key, 'items');
355
- // GitHub provider has mapping for repository_full_name extraction from URL
356
- assert.strictEqual(toolConfig.mappings.url, 'html_url');
353
+ // GitHub preset now uses gh CLI and doesn't need response_key
354
+ // User config custom_field should be merged with preset mappings
355
+ assert.strictEqual(toolConfig.mappings.custom_field, 'some_source');
356
+ // GitHub provider has mappings for gh CLI field normalization
357
+ assert.ok(toolConfig.mappings.html_url, 'Should have html_url mapping');
357
358
  assert.ok(toolConfig.mappings.repository_full_name, 'Should have repository_full_name mapping');
358
359
  });
359
360
 
@@ -534,9 +535,10 @@ sources:
534
535
 
535
536
  assert.strictEqual(sources.length, 1);
536
537
  assert.strictEqual(sources[0].name, 'my-issues');
537
- assert.deepStrictEqual(sources[0].tool, { mcp: 'github', name: 'search_issues' });
538
- assert.strictEqual(sources[0].args.q, 'is:issue assignee:@me state:open');
539
- assert.strictEqual(sources[0].item.id, '{html_url}');
538
+ // GitHub presets now use gh CLI instead of MCP
539
+ assert.ok(Array.isArray(sources[0].tool.command), 'tool.command should be an array');
540
+ assert.ok(sources[0].tool.command.includes('gh'), 'command should use gh CLI');
541
+ assert.strictEqual(sources[0].item.id, '{url}');
540
542
  assert.strictEqual(sources[0].prompt, 'worktree');
541
543
  });
542
544
 
@@ -551,7 +553,8 @@ sources:
551
553
  const sources = getSources();
552
554
 
553
555
  assert.strictEqual(sources[0].name, 'review-requests');
554
- assert.strictEqual(sources[0].args.q, 'is:pr review-requested:@me state:open');
556
+ // GitHub presets now use gh CLI instead of MCP
557
+ assert.ok(sources[0].tool.command.includes('--review-requested=@me'), 'command should include review-requested filter');
555
558
  });
556
559
 
557
560
  test('expands github/my-prs-feedback preset', async () => {
@@ -565,9 +568,11 @@ sources:
565
568
  const sources = getSources();
566
569
 
567
570
  assert.strictEqual(sources[0].name, 'my-prs-feedback');
568
- assert.strictEqual(sources[0].args.q, 'is:pr author:@me state:open comments:>0');
569
- // This preset includes updated_at in reprocess_on to catch new commits
570
- assert.deepStrictEqual(sources[0].reprocess_on, ['state', 'updated_at']);
571
+ // GitHub presets now use gh CLI instead of MCP
572
+ assert.ok(sources[0].tool.command.includes('--author=@me'), 'command should include author filter');
573
+ assert.ok(sources[0].tool.command.includes('comments:>0'), 'command should filter for PRs with comments');
574
+ // This preset includes updatedAt in reprocess_on to catch new commits
575
+ assert.deepStrictEqual(sources[0].reprocess_on, ['state', 'updatedAt']);
571
576
  });
572
577
 
573
578
  test('expands linear/my-issues preset with required args', async () => {
@@ -632,12 +637,12 @@ sources:
632
637
  loadRepoConfig(configPath);
633
638
  const sources = getSources();
634
639
 
635
- // All GitHub presets should have repo field that references repository_full_name
636
- // (which is mapped from repository_url by the GitHub provider)
637
- const mockItem = { repository_full_name: 'myorg/backend' };
640
+ // All GitHub presets should have repo field that references repository.nameWithOwner
641
+ // (gh CLI returns this field directly)
642
+ const mockItem = { repository: { nameWithOwner: 'myorg/backend' } };
638
643
 
639
644
  for (const source of sources) {
640
- assert.strictEqual(source.repo, '{repository_full_name}', `Preset ${source.name} should have repo field`);
645
+ assert.strictEqual(source.repo, '{repository.nameWithOwner}', `Preset ${source.name} should have repo field`);
641
646
  const repos = resolveRepoForItem(source, mockItem);
642
647
  assert.deepStrictEqual(repos, ['myorg/backend'], `Preset ${source.name} should resolve repo from item`);
643
648
  }
@@ -656,12 +661,12 @@ sources:
656
661
  loadRepoConfig(configPath);
657
662
  const source = getSources()[0];
658
663
 
659
- // Item from allowed repo should resolve (repository_full_name is mapped from repository_url)
660
- const allowedItem = { repository_full_name: 'myorg/backend' };
664
+ // Item from allowed repo should resolve (gh CLI returns repository.nameWithOwner)
665
+ const allowedItem = { repository: { nameWithOwner: 'myorg/backend' } };
661
666
  assert.deepStrictEqual(resolveRepoForItem(source, allowedItem), ['myorg/backend']);
662
667
 
663
668
  // Item from non-allowed repo should return empty (filtered out)
664
- const filteredItem = { repository_full_name: 'other/repo' };
669
+ const filteredItem = { repository: { nameWithOwner: 'other/repo' } };
665
670
  assert.deepStrictEqual(resolveRepoForItem(source, filteredItem), []);
666
671
  });
667
672