opencode-pilot 0.24.6 → 0.24.8
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.24.
|
|
5
|
-
sha256 "
|
|
4
|
+
url "https://github.com/athal7/opencode-pilot/archive/refs/tags/v0.24.7.tar.gz"
|
|
5
|
+
sha256 "3d97d488ca8695f9785171e767b8a7a3eb65d2a8c5aca9eab3862b70cf6fc144"
|
|
6
6
|
license "MIT"
|
|
7
7
|
|
|
8
8
|
depends_on "node"
|
package/package.json
CHANGED
package/service/actions.js
CHANGED
|
@@ -639,17 +639,26 @@ export async function sendMessageToSession(serverUrl, sessionId, directory, prom
|
|
|
639
639
|
* @param {string} directory - Working directory to match
|
|
640
640
|
* @param {object} [options] - Options
|
|
641
641
|
* @param {function} [options.fetch] - Custom fetch function (for testing)
|
|
642
|
+
* @param {string} [options.projectDirectory] - Project directory for session lookup
|
|
643
|
+
* (used instead of directory when provided, so sessions created with the project
|
|
644
|
+
* directory are found instead of old worktree-scoped sessions)
|
|
642
645
|
* @returns {Promise<object|null>} Session to reuse, or null
|
|
643
646
|
*/
|
|
644
647
|
export async function findReusableSession(serverUrl, directory, options = {}) {
|
|
648
|
+
// Use project directory for session lookup when available.
|
|
649
|
+
// Sessions created with v0.24.7+ are scoped to the project directory,
|
|
650
|
+
// while messages use the worktree directory. Query the project directory
|
|
651
|
+
// to find correctly-scoped sessions.
|
|
652
|
+
const lookupDirectory = options.projectDirectory || directory;
|
|
653
|
+
|
|
645
654
|
// Get sessions for this directory
|
|
646
655
|
const sessions = await listSessions(serverUrl, {
|
|
647
|
-
directory,
|
|
656
|
+
directory: lookupDirectory,
|
|
648
657
|
fetch: options.fetch
|
|
649
658
|
});
|
|
650
659
|
|
|
651
660
|
if (sessions.length === 0) {
|
|
652
|
-
debug(`findReusableSession: no sessions found for ${
|
|
661
|
+
debug(`findReusableSession: no sessions found for ${lookupDirectory}`);
|
|
653
662
|
return null;
|
|
654
663
|
}
|
|
655
664
|
|
|
@@ -657,11 +666,11 @@ export async function findReusableSession(serverUrl, directory, options = {}) {
|
|
|
657
666
|
const activeSessions = sessions.filter(s => !isSessionArchived(s));
|
|
658
667
|
|
|
659
668
|
if (activeSessions.length === 0) {
|
|
660
|
-
debug(`findReusableSession: all ${sessions.length} sessions are archived for ${
|
|
669
|
+
debug(`findReusableSession: all ${sessions.length} sessions are archived for ${lookupDirectory}`);
|
|
661
670
|
return null;
|
|
662
671
|
}
|
|
663
672
|
|
|
664
|
-
debug(`findReusableSession: found ${activeSessions.length} active sessions for ${
|
|
673
|
+
debug(`findReusableSession: found ${activeSessions.length} active sessions for ${lookupDirectory}`);
|
|
665
674
|
|
|
666
675
|
// Get statuses to prefer idle sessions
|
|
667
676
|
const statuses = await getSessionStatuses(serverUrl, { fetch: options.fetch });
|
|
@@ -673,14 +682,11 @@ export async function findReusableSession(serverUrl, directory, options = {}) {
|
|
|
673
682
|
/**
|
|
674
683
|
* Create a session via the OpenCode HTTP API
|
|
675
684
|
*
|
|
676
|
-
* This is a workaround for the known issue where `opencode run --attach`
|
|
677
|
-
* doesn't support a --dir flag, causing sessions to run in the wrong directory
|
|
678
|
-
* when attached to a global server.
|
|
679
|
-
*
|
|
680
685
|
* @param {string} serverUrl - Server URL (e.g., "http://localhost:4096")
|
|
681
|
-
* @param {string} directory - Working directory for
|
|
686
|
+
* @param {string} directory - Working directory for file operations (may be a worktree)
|
|
682
687
|
* @param {string} prompt - The prompt/message to send
|
|
683
688
|
* @param {object} [options] - Options
|
|
689
|
+
* @param {string} [options.projectDirectory] - Project directory for session scoping (defaults to directory)
|
|
684
690
|
* @param {string} [options.title] - Session title
|
|
685
691
|
* @param {string} [options.agent] - Agent to use
|
|
686
692
|
* @param {string} [options.model] - Model to use
|
|
@@ -690,13 +696,16 @@ export async function findReusableSession(serverUrl, directory, options = {}) {
|
|
|
690
696
|
export async function createSessionViaApi(serverUrl, directory, prompt, options = {}) {
|
|
691
697
|
const fetchFn = options.fetch || fetch;
|
|
692
698
|
const headerTimeout = options.headerTimeout || HEADER_TIMEOUT_MS;
|
|
699
|
+
// Use project directory for session creation (determines projectID in OpenCode).
|
|
700
|
+
// The working directory (which may be a worktree) is used for messages/commands.
|
|
701
|
+
const projectDir = options.projectDirectory || directory;
|
|
693
702
|
|
|
694
703
|
let session = null;
|
|
695
704
|
|
|
696
705
|
try {
|
|
697
|
-
// Step 1: Create
|
|
706
|
+
// Step 1: Create session scoped to the project directory
|
|
698
707
|
const sessionUrl = new URL('/session', serverUrl);
|
|
699
|
-
sessionUrl.searchParams.set('directory',
|
|
708
|
+
sessionUrl.searchParams.set('directory', projectDir);
|
|
700
709
|
|
|
701
710
|
const createResponse = await fetchFn(sessionUrl.toString(), {
|
|
702
711
|
method: 'POST',
|
|
@@ -715,7 +724,7 @@ export async function createSessionViaApi(serverUrl, directory, prompt, options
|
|
|
715
724
|
// Step 2: Update session title if provided
|
|
716
725
|
if (options.title) {
|
|
717
726
|
const updateUrl = new URL(`/session/${session.id}`, serverUrl);
|
|
718
|
-
updateUrl.searchParams.set('directory',
|
|
727
|
+
updateUrl.searchParams.set('directory', projectDir);
|
|
719
728
|
await fetchFn(updateUrl.toString(), {
|
|
720
729
|
method: 'PATCH',
|
|
721
730
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -842,7 +851,7 @@ export async function createSessionViaApi(serverUrl, directory, prompt, options
|
|
|
842
851
|
* @param {object} [options] - Execution options
|
|
843
852
|
* @returns {Promise<object>} Result with command, success, sessionId, etc.
|
|
844
853
|
*/
|
|
845
|
-
async function executeInDirectory(serverUrl, cwd, item, config, options = {}) {
|
|
854
|
+
async function executeInDirectory(serverUrl, cwd, item, config, options = {}, projectDirectory = null) {
|
|
846
855
|
// Build prompt from template
|
|
847
856
|
const prompt = buildPromptFromTemplate(config.prompt || "default", item);
|
|
848
857
|
|
|
@@ -886,7 +895,10 @@ async function executeInDirectory(serverUrl, cwd, item, config, options = {}) {
|
|
|
886
895
|
const reuseActiveSession = config.reuse_active_session !== false; // default true
|
|
887
896
|
|
|
888
897
|
if (reuseActiveSession && !options.dryRun) {
|
|
889
|
-
const existingSession = await findReusableSession(serverUrl, cwd, {
|
|
898
|
+
const existingSession = await findReusableSession(serverUrl, cwd, {
|
|
899
|
+
fetch: options.fetch,
|
|
900
|
+
projectDirectory: projectDirectory,
|
|
901
|
+
});
|
|
890
902
|
|
|
891
903
|
if (existingSession) {
|
|
892
904
|
debug(`executeInDirectory: found reusable session ${existingSession.id} for ${cwd}`);
|
|
@@ -923,6 +935,7 @@ async function executeInDirectory(serverUrl, cwd, item, config, options = {}) {
|
|
|
923
935
|
}
|
|
924
936
|
|
|
925
937
|
const result = await createSessionViaApi(serverUrl, cwd, prompt, {
|
|
938
|
+
projectDirectory: projectDirectory || cwd,
|
|
926
939
|
title: sessionTitle,
|
|
927
940
|
agent: config.agent,
|
|
928
941
|
model: config.model,
|
|
@@ -986,7 +999,7 @@ export async function executeAction(item, config, options = {}) {
|
|
|
986
999
|
if (config.existing_directory) {
|
|
987
1000
|
debug(`executeAction: using existing_directory=${config.existing_directory}`);
|
|
988
1001
|
const cwd = expandPath(config.existing_directory);
|
|
989
|
-
return await executeInDirectory(serverUrl, cwd, item, config, options);
|
|
1002
|
+
return await executeInDirectory(serverUrl, cwd, item, config, options, baseCwd);
|
|
990
1003
|
}
|
|
991
1004
|
|
|
992
1005
|
// Resolve worktree directory if configured
|
|
@@ -1038,5 +1051,5 @@ export async function executeAction(item, config, options = {}) {
|
|
|
1038
1051
|
|
|
1039
1052
|
debug(`executeAction: using cwd=${cwd}`);
|
|
1040
1053
|
|
|
1041
|
-
return await executeInDirectory(serverUrl, cwd, item, config, options);
|
|
1054
|
+
return await executeInDirectory(serverUrl, cwd, item, config, options, baseCwd);
|
|
1042
1055
|
}
|
|
@@ -564,7 +564,9 @@ describe("integration: worktree creation with worktree_name", () => {
|
|
|
564
564
|
assert.ok(worktreeCreateCalled, "Should create worktree when worktree_name is configured");
|
|
565
565
|
assert.strictEqual(createdWorktreeName, "pr-42", "Should expand worktree_name template");
|
|
566
566
|
assert.ok(sessionCreated, "Should create session");
|
|
567
|
-
|
|
567
|
+
// Session creation uses the project directory (for correct projectID scoping)
|
|
568
|
+
// The worktree path is used for messages/commands (file operations)
|
|
569
|
+
assert.strictEqual(sessionDirectory, "/proj", "Session should be scoped to project directory");
|
|
568
570
|
});
|
|
569
571
|
|
|
570
572
|
it("reuses stored directory when reprocessing same item", async () => {
|
|
@@ -616,8 +618,73 @@ describe("integration: worktree creation with worktree_name", () => {
|
|
|
616
618
|
assert.ok(result.success, "Action should succeed");
|
|
617
619
|
// Should NOT create a new worktree since we have existing_directory
|
|
618
620
|
assert.strictEqual(worktreeCreateCalled, false, "Should NOT create new worktree when existing_directory provided");
|
|
619
|
-
// Session
|
|
620
|
-
assert.strictEqual(sessionDirectory,
|
|
621
|
+
// Session creation uses the project directory (for correct projectID scoping)
|
|
622
|
+
assert.strictEqual(sessionDirectory, "/proj", "Session should be scoped to project directory");
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it("session reuse queries by project directory, not worktree directory", async () => {
|
|
626
|
+
// This tests the key fix: when reprocessing an item with a worktree,
|
|
627
|
+
// findReusableSession should query by the project directory (e.g., /proj)
|
|
628
|
+
// so it finds sessions created with correct scoping (v0.24.7+), rather
|
|
629
|
+
// than finding old sessions created with the worktree directory (projectID "global").
|
|
630
|
+
|
|
631
|
+
let sessionQueryDirectory = null;
|
|
632
|
+
let sessionCreated = false;
|
|
633
|
+
|
|
634
|
+
const existingWorktreeDir = "/worktree/calm-wizard";
|
|
635
|
+
|
|
636
|
+
mockServer = await createMockServer({
|
|
637
|
+
"GET /project": () => ({
|
|
638
|
+
body: [{ id: "proj_1", worktree: "/proj", sandboxes: [], time: { created: 1 } }],
|
|
639
|
+
}),
|
|
640
|
+
"GET /project/current": () => ({
|
|
641
|
+
body: { id: "proj_1", worktree: "/proj", sandboxes: [], time: { created: 1 } },
|
|
642
|
+
}),
|
|
643
|
+
"GET /experimental/worktree": () => ({
|
|
644
|
+
body: [existingWorktreeDir],
|
|
645
|
+
}),
|
|
646
|
+
"GET /session": (req) => {
|
|
647
|
+
sessionQueryDirectory = req.directory;
|
|
648
|
+
// Return a session ONLY if queried by project directory
|
|
649
|
+
if (req.directory === "/proj") {
|
|
650
|
+
return {
|
|
651
|
+
body: [{ id: "ses_proj_scoped", directory: "/proj", time: { created: 1000, updated: 2000 } }],
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
// Old worktree-scoped sessions should NOT be found when querying by project dir
|
|
655
|
+
return { body: [] };
|
|
656
|
+
},
|
|
657
|
+
"GET /session/status": () => ({ body: {} }),
|
|
658
|
+
"POST /session": (req) => {
|
|
659
|
+
sessionCreated = true;
|
|
660
|
+
return { body: { id: "ses_new" } };
|
|
661
|
+
},
|
|
662
|
+
"PATCH /session/ses_proj_scoped": () => ({ body: {} }),
|
|
663
|
+
"POST /session/ses_proj_scoped/message": () => ({ body: { success: true } }),
|
|
664
|
+
"POST /session/ses_proj_scoped/command": () => ({ body: { success: true } }),
|
|
665
|
+
"PATCH /session/ses_new": () => ({ body: {} }),
|
|
666
|
+
"POST /session/ses_new/message": () => ({ body: { success: true } }),
|
|
667
|
+
"POST /session/ses_new/command": () => ({ body: { success: true } }),
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
const result = await executeAction(
|
|
671
|
+
{ number: 42, title: "Review PR" },
|
|
672
|
+
{
|
|
673
|
+
path: "/proj",
|
|
674
|
+
prompt: "review",
|
|
675
|
+
worktree_name: "pr-{number}",
|
|
676
|
+
existing_directory: existingWorktreeDir,
|
|
677
|
+
},
|
|
678
|
+
{ discoverServer: async () => mockServer.url }
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
assert.ok(result.success, "Action should succeed");
|
|
682
|
+
// The session lookup should use the PROJECT directory, not the worktree directory
|
|
683
|
+
assert.strictEqual(sessionQueryDirectory, "/proj",
|
|
684
|
+
"findReusableSession should query by project directory, not worktree");
|
|
685
|
+
// Should reuse the project-scoped session, NOT create a new one
|
|
686
|
+
assert.strictEqual(result.sessionReused, true, "Should reuse the project-scoped session");
|
|
687
|
+
assert.strictEqual(sessionCreated, false, "Should NOT create a new session when project-scoped session exists");
|
|
621
688
|
});
|
|
622
689
|
});
|
|
623
690
|
|
|
@@ -981,11 +981,12 @@ Check for bugs and security issues.`;
|
|
|
981
981
|
// Should NOT call worktree endpoints when existing_directory is provided
|
|
982
982
|
assert.strictEqual(worktreeListCalled, false, 'Should NOT list worktrees');
|
|
983
983
|
assert.strictEqual(worktreeCreateCalled, false, 'Should NOT create worktree');
|
|
984
|
-
//
|
|
985
|
-
assert.strictEqual(sessionDirectory, '/data/
|
|
986
|
-
'
|
|
984
|
+
// Session creation uses project directory (for correct projectID scoping)
|
|
985
|
+
assert.strictEqual(sessionDirectory, '/data/proj',
|
|
986
|
+
'Session creation should use project directory, not worktree');
|
|
987
|
+
// Result directory is the worktree (where file operations happen)
|
|
987
988
|
assert.strictEqual(result.directory, '/data/worktree/calm-wizard',
|
|
988
|
-
'Result should include directory');
|
|
989
|
+
'Result should include worktree directory');
|
|
989
990
|
});
|
|
990
991
|
});
|
|
991
992
|
|
|
@@ -1041,6 +1042,52 @@ Check for bugs and security issues.`;
|
|
|
1041
1042
|
assert.ok(messageUrl.includes('%2Fpath%2Fto%2Fproject'), 'Message URL should include encoded directory path');
|
|
1042
1043
|
});
|
|
1043
1044
|
|
|
1045
|
+
test('uses projectDirectory for session creation, working directory for messages', async () => {
|
|
1046
|
+
const { createSessionViaApi } = await import('../../service/actions.js');
|
|
1047
|
+
|
|
1048
|
+
const mockSessionId = 'ses_test_proj';
|
|
1049
|
+
let createUrl = null;
|
|
1050
|
+
let messageUrl = null;
|
|
1051
|
+
|
|
1052
|
+
const mockFetch = async (url, opts) => {
|
|
1053
|
+
const urlObj = new URL(url);
|
|
1054
|
+
|
|
1055
|
+
if (urlObj.pathname === '/session' && opts?.method === 'POST') {
|
|
1056
|
+
createUrl = url;
|
|
1057
|
+
return { ok: true, json: async () => ({ id: mockSessionId }) };
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
if (urlObj.pathname.includes('/message') && opts?.method === 'POST') {
|
|
1061
|
+
messageUrl = url;
|
|
1062
|
+
return { ok: true, json: async () => ({ success: true }) };
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
return { ok: false, text: async () => 'Not found' };
|
|
1066
|
+
};
|
|
1067
|
+
|
|
1068
|
+
await createSessionViaApi(
|
|
1069
|
+
'http://localhost:4096',
|
|
1070
|
+
'/home/user/.local/share/opencode/worktree/abc123/pr-415',
|
|
1071
|
+
'Fix the bug',
|
|
1072
|
+
{
|
|
1073
|
+
fetch: mockFetch,
|
|
1074
|
+
projectDirectory: '/home/user/code/odin',
|
|
1075
|
+
}
|
|
1076
|
+
);
|
|
1077
|
+
|
|
1078
|
+
// Session creation should use the project directory (for correct projectID scoping)
|
|
1079
|
+
assert.ok(createUrl.includes('%2Fhome%2Fuser%2Fcode%2Fodin'),
|
|
1080
|
+
'Session creation should use projectDirectory');
|
|
1081
|
+
assert.ok(!createUrl.includes('worktree'),
|
|
1082
|
+
'Session creation should NOT use the worktree path');
|
|
1083
|
+
|
|
1084
|
+
// Message should use the working directory (for file operations in the worktree)
|
|
1085
|
+
assert.ok(messageUrl.includes('worktree'),
|
|
1086
|
+
'Message should use the worktree working directory');
|
|
1087
|
+
assert.ok(messageUrl.includes('pr-415'),
|
|
1088
|
+
'Message should use the worktree working directory');
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1044
1091
|
test('handles session creation failure', async () => {
|
|
1045
1092
|
const { createSessionViaApi } = await import('../../service/actions.js');
|
|
1046
1093
|
|
|
@@ -2100,6 +2147,60 @@ Check for bugs and security issues.`;
|
|
|
2100
2147
|
assert.strictEqual(result.id, 'ses_new');
|
|
2101
2148
|
});
|
|
2102
2149
|
|
|
2150
|
+
test('uses projectDirectory for session lookup when provided', async () => {
|
|
2151
|
+
const { findReusableSession } = await import('../../service/actions.js');
|
|
2152
|
+
|
|
2153
|
+
let queriedDirectory = null;
|
|
2154
|
+
const mockFetch = async (url) => {
|
|
2155
|
+
if (url.includes('/session/status')) {
|
|
2156
|
+
return { ok: true, json: async () => ({}) };
|
|
2157
|
+
}
|
|
2158
|
+
// Capture the directory used in the session query
|
|
2159
|
+
const urlObj = new URL(url);
|
|
2160
|
+
queriedDirectory = urlObj.searchParams.get('directory');
|
|
2161
|
+
return {
|
|
2162
|
+
ok: true,
|
|
2163
|
+
json: async () => [
|
|
2164
|
+
{ id: 'ses_proj', time: { created: 1000, updated: 2000 } },
|
|
2165
|
+
],
|
|
2166
|
+
};
|
|
2167
|
+
};
|
|
2168
|
+
|
|
2169
|
+
const result = await findReusableSession('http://localhost:4096', '/worktree/pr-415', {
|
|
2170
|
+
fetch: mockFetch,
|
|
2171
|
+
projectDirectory: '/home/user/code/odin',
|
|
2172
|
+
});
|
|
2173
|
+
|
|
2174
|
+
assert.strictEqual(result.id, 'ses_proj');
|
|
2175
|
+
assert.strictEqual(queriedDirectory, '/home/user/code/odin',
|
|
2176
|
+
'Should query sessions using projectDirectory, not the worktree directory');
|
|
2177
|
+
});
|
|
2178
|
+
|
|
2179
|
+
test('falls back to directory when projectDirectory not provided', async () => {
|
|
2180
|
+
const { findReusableSession } = await import('../../service/actions.js');
|
|
2181
|
+
|
|
2182
|
+
let queriedDirectory = null;
|
|
2183
|
+
const mockFetch = async (url) => {
|
|
2184
|
+
if (url.includes('/session/status')) {
|
|
2185
|
+
return { ok: true, json: async () => ({}) };
|
|
2186
|
+
}
|
|
2187
|
+
const urlObj = new URL(url);
|
|
2188
|
+
queriedDirectory = urlObj.searchParams.get('directory');
|
|
2189
|
+
return {
|
|
2190
|
+
ok: true,
|
|
2191
|
+
json: async () => [
|
|
2192
|
+
{ id: 'ses_1', time: { created: 1000, updated: 2000 } },
|
|
2193
|
+
],
|
|
2194
|
+
};
|
|
2195
|
+
};
|
|
2196
|
+
|
|
2197
|
+
const result = await findReusableSession('http://localhost:4096', '/test/dir', { fetch: mockFetch });
|
|
2198
|
+
|
|
2199
|
+
assert.strictEqual(result.id, 'ses_1');
|
|
2200
|
+
assert.strictEqual(queriedDirectory, '/test/dir',
|
|
2201
|
+
'Should query sessions using directory when projectDirectory not provided');
|
|
2202
|
+
});
|
|
2203
|
+
|
|
2103
2204
|
test('falls back to busy session when no idle available', async () => {
|
|
2104
2205
|
const { findReusableSession } = await import('../../service/actions.js');
|
|
2105
2206
|
|