cortex-agents 3.4.0 → 4.0.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/.opencode/agents/architect.md +82 -89
- package/.opencode/agents/audit.md +57 -188
- package/.opencode/agents/{crosslayer.md → coder.md} +9 -52
- package/.opencode/agents/debug.md +151 -0
- package/.opencode/agents/devops.md +142 -0
- package/.opencode/agents/docs-writer.md +195 -0
- package/.opencode/agents/fix.md +119 -189
- package/.opencode/agents/implement.md +115 -74
- package/.opencode/agents/perf.md +151 -0
- package/.opencode/agents/refactor.md +163 -0
- package/.opencode/agents/{guard.md → security.md} +20 -85
- package/.opencode/agents/testing.md +115 -0
- package/.opencode/skills/data-engineering/SKILL.md +221 -0
- package/.opencode/skills/monitoring-observability/SKILL.md +251 -0
- package/.opencode/skills/ui-design/SKILL.md +402 -0
- package/README.md +303 -287
- package/dist/cli.js +6 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -28
- package/dist/registry.d.ts +4 -4
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +6 -6
- package/dist/tools/branch.d.ts +2 -2
- package/dist/tools/docs.d.ts +2 -2
- package/dist/tools/github.d.ts +3 -3
- package/dist/tools/plan.d.ts +28 -4
- package/dist/tools/plan.d.ts.map +1 -1
- package/dist/tools/plan.js +232 -4
- package/dist/tools/quality-gate.d.ts +28 -0
- package/dist/tools/quality-gate.d.ts.map +1 -0
- package/dist/tools/quality-gate.js +233 -0
- package/dist/tools/repl.d.ts +5 -0
- package/dist/tools/repl.d.ts.map +1 -1
- package/dist/tools/repl.js +58 -7
- package/dist/tools/worktree.d.ts +5 -32
- package/dist/tools/worktree.d.ts.map +1 -1
- package/dist/tools/worktree.js +75 -458
- package/dist/utils/change-scope.d.ts +33 -0
- package/dist/utils/change-scope.d.ts.map +1 -0
- package/dist/utils/change-scope.js +198 -0
- package/dist/utils/plan-extract.d.ts +21 -0
- package/dist/utils/plan-extract.d.ts.map +1 -1
- package/dist/utils/plan-extract.js +65 -0
- package/dist/utils/repl.d.ts +31 -0
- package/dist/utils/repl.d.ts.map +1 -1
- package/dist/utils/repl.js +126 -13
- package/package.json +1 -1
- package/.opencode/agents/qa.md +0 -265
- package/.opencode/agents/ship.md +0 -249
package/dist/tools/worktree.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin";
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
|
-
import {
|
|
5
|
-
import { git, which, shellEscape, kill, spawn as shellSpawn } from "../utils/shell.js";
|
|
6
|
-
import { detectTerminalDriver, closeSession, writeSession, readSession, } from "../utils/terminal.js";
|
|
4
|
+
import { git } from "../utils/shell.js";
|
|
7
5
|
const WORKTREE_ROOT = ".worktrees";
|
|
8
6
|
/**
|
|
9
7
|
* Factory function that creates the worktree_create tool with access
|
|
@@ -19,10 +17,15 @@ export function createCreate(client) {
|
|
|
19
17
|
type: tool.schema
|
|
20
18
|
.enum(["feature", "bugfix", "hotfix", "refactor", "spike", "docs", "test"])
|
|
21
19
|
.describe("Type of work - determines branch prefix"),
|
|
20
|
+
fromBranch: tool.schema
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Use an existing branch instead of creating a new one " +
|
|
24
|
+
"(e.g., 'feature/auth' from plan_commit). If set, skips branch creation."),
|
|
22
25
|
},
|
|
23
26
|
async execute(args, context) {
|
|
24
|
-
const { name, type } = args;
|
|
25
|
-
const branchName = `${type}/${name}`;
|
|
27
|
+
const { name, type, fromBranch } = args;
|
|
28
|
+
const branchName = fromBranch || `${type}/${name}`;
|
|
26
29
|
const worktreePath = path.join(context.worktree, WORKTREE_ROOT, name);
|
|
27
30
|
const absoluteWorktreePath = path.resolve(worktreePath);
|
|
28
31
|
// Check if we're in a git repository
|
|
@@ -30,11 +33,11 @@ export function createCreate(client) {
|
|
|
30
33
|
await git(context.worktree, "rev-parse", "--git-dir");
|
|
31
34
|
}
|
|
32
35
|
catch {
|
|
33
|
-
return "
|
|
36
|
+
return "\u2717 Error: Not in a git repository. Initialize git first.";
|
|
34
37
|
}
|
|
35
38
|
// Check if worktree already exists
|
|
36
39
|
if (fs.existsSync(absoluteWorktreePath)) {
|
|
37
|
-
return
|
|
40
|
+
return `\u2717 Error: Worktree already exists at ${absoluteWorktreePath}
|
|
38
41
|
|
|
39
42
|
Use worktree_list to see existing worktrees.`;
|
|
40
43
|
}
|
|
@@ -43,38 +46,70 @@ Use worktree_list to see existing worktrees.`;
|
|
|
43
46
|
if (!fs.existsSync(worktreeParent)) {
|
|
44
47
|
fs.mkdirSync(worktreeParent, { recursive: true });
|
|
45
48
|
}
|
|
46
|
-
// Create the worktree
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
// Create the worktree
|
|
50
|
+
if (fromBranch) {
|
|
51
|
+
// Use existing branch — try directly first
|
|
52
|
+
try {
|
|
53
|
+
await git(context.worktree, "worktree", "add", absoluteWorktreePath, fromBranch);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Branch might not exist yet — create it
|
|
57
|
+
try {
|
|
58
|
+
await git(context.worktree, "worktree", "add", "-b", fromBranch, absoluteWorktreePath);
|
|
59
|
+
}
|
|
60
|
+
catch (error2) {
|
|
61
|
+
try {
|
|
62
|
+
await client.tui.showToast({
|
|
63
|
+
body: {
|
|
64
|
+
title: `Worktree: ${name}`,
|
|
65
|
+
message: `Failed to create from branch '${fromBranch}': ${error2.message || error2}`,
|
|
66
|
+
variant: "error",
|
|
67
|
+
duration: 8000,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Toast failure is non-fatal
|
|
73
|
+
}
|
|
74
|
+
return `\u2717 Error creating worktree from branch '${fromBranch}': ${error2.message || error2}`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
49
77
|
}
|
|
50
|
-
|
|
51
|
-
//
|
|
78
|
+
else {
|
|
79
|
+
// Create with a new branch
|
|
52
80
|
try {
|
|
53
|
-
await git(context.worktree, "worktree", "add",
|
|
81
|
+
await git(context.worktree, "worktree", "add", "-b", branchName, absoluteWorktreePath);
|
|
54
82
|
}
|
|
55
|
-
catch
|
|
83
|
+
catch {
|
|
84
|
+
// Branch might already exist, try without -b
|
|
56
85
|
try {
|
|
57
|
-
await
|
|
58
|
-
body: {
|
|
59
|
-
title: `Worktree: ${name}`,
|
|
60
|
-
message: `Failed to create: ${error2.message || error2}`,
|
|
61
|
-
variant: "error",
|
|
62
|
-
duration: 8000,
|
|
63
|
-
},
|
|
64
|
-
});
|
|
86
|
+
await git(context.worktree, "worktree", "add", absoluteWorktreePath, branchName);
|
|
65
87
|
}
|
|
66
|
-
catch {
|
|
67
|
-
|
|
88
|
+
catch (error2) {
|
|
89
|
+
try {
|
|
90
|
+
await client.tui.showToast({
|
|
91
|
+
body: {
|
|
92
|
+
title: `Worktree: ${name}`,
|
|
93
|
+
message: `Failed to create: ${error2.message || error2}`,
|
|
94
|
+
variant: "error",
|
|
95
|
+
duration: 8000,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Toast failure is non-fatal
|
|
101
|
+
}
|
|
102
|
+
return `\u2717 Error creating worktree: ${error2.message || error2}`;
|
|
68
103
|
}
|
|
69
|
-
return `✗ Error creating worktree: ${error2.message || error2}`;
|
|
70
104
|
}
|
|
71
105
|
}
|
|
72
106
|
// Notify via toast
|
|
107
|
+
const fromLabel = fromBranch ? ` (from existing branch)` : "";
|
|
73
108
|
try {
|
|
74
109
|
await client.tui.showToast({
|
|
75
110
|
body: {
|
|
76
111
|
title: `Worktree: ${name}`,
|
|
77
|
-
message: `Created on branch ${branchName}`,
|
|
112
|
+
message: `Created on branch ${branchName}${fromLabel}`,
|
|
78
113
|
variant: "success",
|
|
79
114
|
duration: 4000,
|
|
80
115
|
},
|
|
@@ -83,9 +118,9 @@ Use worktree_list to see existing worktrees.`;
|
|
|
83
118
|
catch {
|
|
84
119
|
// Toast failure is non-fatal
|
|
85
120
|
}
|
|
86
|
-
return
|
|
121
|
+
return `\u2713 Created worktree successfully
|
|
87
122
|
|
|
88
|
-
Branch: ${branchName}
|
|
123
|
+
Branch: ${branchName}${fromLabel}
|
|
89
124
|
Path: ${absoluteWorktreePath}
|
|
90
125
|
|
|
91
126
|
To work in this worktree:
|
|
@@ -113,20 +148,20 @@ export const list = tool({
|
|
|
113
148
|
const branch = parts[2]?.replace(/[\[\]]/g, "") || "detached";
|
|
114
149
|
const isMain = worktreePath === context.worktree;
|
|
115
150
|
const marker = isMain ? " (main)" : "";
|
|
116
|
-
output +=
|
|
151
|
+
output += `\u2022 ${branch}${marker}\n`;
|
|
117
152
|
output += ` Path: ${worktreePath}\n`;
|
|
118
153
|
output += ` Commit: ${commit}\n\n`;
|
|
119
154
|
}
|
|
120
155
|
return output.trim();
|
|
121
156
|
}
|
|
122
157
|
catch (error) {
|
|
123
|
-
return
|
|
158
|
+
return `\u2717 Error listing worktrees: ${error.message || error}`;
|
|
124
159
|
}
|
|
125
160
|
},
|
|
126
161
|
});
|
|
127
162
|
/**
|
|
128
163
|
* Factory function that creates the worktree_remove tool with access
|
|
129
|
-
* to the OpenCode client for toast notifications
|
|
164
|
+
* to the OpenCode client for toast notifications.
|
|
130
165
|
*/
|
|
131
166
|
export function createRemove(client) {
|
|
132
167
|
return tool({
|
|
@@ -144,7 +179,7 @@ export function createRemove(client) {
|
|
|
144
179
|
const absoluteWorktreePath = path.resolve(worktreePath);
|
|
145
180
|
// Check if worktree exists
|
|
146
181
|
if (!fs.existsSync(absoluteWorktreePath)) {
|
|
147
|
-
return
|
|
182
|
+
return `\u2717 Error: Worktree not found at ${absoluteWorktreePath}
|
|
148
183
|
|
|
149
184
|
Use worktree_list to see existing worktrees.`;
|
|
150
185
|
}
|
|
@@ -157,45 +192,7 @@ Use worktree_list to see existing worktrees.`;
|
|
|
157
192
|
catch {
|
|
158
193
|
// Ignore error, branch detection is optional
|
|
159
194
|
}
|
|
160
|
-
//
|
|
161
|
-
let closedSession = false;
|
|
162
|
-
const session = readSession(absoluteWorktreePath);
|
|
163
|
-
if (session) {
|
|
164
|
-
if (session.mode === "pty" && session.ptyId) {
|
|
165
|
-
// Close PTY session via OpenCode SDK
|
|
166
|
-
try {
|
|
167
|
-
await client.pty.remove({ path: { id: session.ptyId } });
|
|
168
|
-
closedSession = true;
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
// PTY may already be closed
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
else if (session.mode === "terminal" || session.mode === "ide") {
|
|
175
|
-
// Close terminal/IDE tab via driver
|
|
176
|
-
closedSession = await closeSession(session);
|
|
177
|
-
}
|
|
178
|
-
else if (session.mode === "background" && session.pid) {
|
|
179
|
-
closedSession = kill(session.pid);
|
|
180
|
-
}
|
|
181
|
-
// Fallback: kill PID if driver close failed
|
|
182
|
-
if (!closedSession && session.pid) {
|
|
183
|
-
kill(session.pid);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
// Also clean up legacy .background-pid file
|
|
187
|
-
const bgPidFile = path.join(absoluteWorktreePath, ".cortex", ".background-pid");
|
|
188
|
-
if (fs.existsSync(bgPidFile)) {
|
|
189
|
-
try {
|
|
190
|
-
const bgData = JSON.parse(fs.readFileSync(bgPidFile, "utf-8"));
|
|
191
|
-
if (bgData.pid)
|
|
192
|
-
kill(bgData.pid);
|
|
193
|
-
}
|
|
194
|
-
catch {
|
|
195
|
-
// Ignore parse errors
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
// ── Remove the worktree ─────────────────────────────────────
|
|
195
|
+
// Remove the worktree
|
|
199
196
|
try {
|
|
200
197
|
await git(context.worktree, "worktree", "remove", absoluteWorktreePath);
|
|
201
198
|
}
|
|
@@ -218,35 +215,31 @@ Use worktree_list to see existing worktrees.`;
|
|
|
218
215
|
catch {
|
|
219
216
|
// Toast failure is non-fatal
|
|
220
217
|
}
|
|
221
|
-
return
|
|
218
|
+
return `\u2717 Error removing worktree: ${error2.message || error2}
|
|
222
219
|
|
|
223
220
|
The worktree may have uncommitted changes. Commit or stash them first.`;
|
|
224
221
|
}
|
|
225
222
|
}
|
|
226
|
-
let output =
|
|
227
|
-
if (closedSession && session) {
|
|
228
|
-
output += `\n✓ Closed ${session.mode === "pty" ? "PTY session" : `${session.terminal} tab`}`;
|
|
229
|
-
}
|
|
223
|
+
let output = `\u2713 Removed worktree at ${absoluteWorktreePath}`;
|
|
230
224
|
// Delete branch if requested
|
|
231
225
|
if (deleteBranch && branchName) {
|
|
232
226
|
try {
|
|
233
227
|
await git(context.worktree, "branch", "-d", branchName);
|
|
234
|
-
output += `\n
|
|
228
|
+
output += `\n\u2713 Deleted branch ${branchName}`;
|
|
235
229
|
}
|
|
236
230
|
catch (error) {
|
|
237
|
-
output += `\n
|
|
231
|
+
output += `\n\u26A0 Could not delete branch ${branchName}: ${error.message || error}`;
|
|
238
232
|
output += "\n (Branch may not be fully merged. Use git branch -D to force delete.)";
|
|
239
233
|
}
|
|
240
234
|
}
|
|
241
235
|
// Notify via toast
|
|
242
236
|
try {
|
|
243
|
-
const closedInfo = closedSession ? " + closed tab" : "";
|
|
244
237
|
await client.tui.showToast({
|
|
245
238
|
body: {
|
|
246
239
|
title: `Worktree: ${name}`,
|
|
247
240
|
message: branchName
|
|
248
|
-
? `Removed worktree
|
|
249
|
-
: `Removed worktree
|
|
241
|
+
? `Removed worktree (branch ${branchName} ${deleteBranch ? "deleted" : "kept"})`
|
|
242
|
+
: `Removed worktree`,
|
|
250
243
|
variant: "success",
|
|
251
244
|
duration: 4000,
|
|
252
245
|
},
|
|
@@ -270,7 +263,7 @@ export const open = tool({
|
|
|
270
263
|
const absoluteWorktreePath = path.resolve(worktreePath);
|
|
271
264
|
// Check if worktree exists
|
|
272
265
|
if (!fs.existsSync(absoluteWorktreePath)) {
|
|
273
|
-
return
|
|
266
|
+
return `\u2717 Error: Worktree not found at ${absoluteWorktreePath}
|
|
274
267
|
|
|
275
268
|
Use worktree_list to see existing worktrees.`;
|
|
276
269
|
}
|
|
@@ -306,379 +299,3 @@ ${instructions}
|
|
|
306
299
|
Worktree path: ${absoluteWorktreePath}`;
|
|
307
300
|
},
|
|
308
301
|
});
|
|
309
|
-
// ─── Terminal detection and tab management now in src/utils/terminal.ts ──────
|
|
310
|
-
/**
|
|
311
|
-
* Find the opencode binary path, checking common locations.
|
|
312
|
-
*/
|
|
313
|
-
async function findOpencodeBinary() {
|
|
314
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
315
|
-
// Check well-known path first
|
|
316
|
-
const wellKnown = path.join(homeDir, ".opencode", "bin", "opencode");
|
|
317
|
-
if (fs.existsSync(wellKnown))
|
|
318
|
-
return wellKnown;
|
|
319
|
-
// Try which
|
|
320
|
-
const bin = await which("opencode");
|
|
321
|
-
if (bin && fs.existsSync(bin))
|
|
322
|
-
return bin;
|
|
323
|
-
return null;
|
|
324
|
-
}
|
|
325
|
-
/**
|
|
326
|
-
* Build the prompt string for the new OpenCode session.
|
|
327
|
-
*/
|
|
328
|
-
function buildLaunchPrompt(planFilename, customPrompt) {
|
|
329
|
-
if (customPrompt)
|
|
330
|
-
return customPrompt;
|
|
331
|
-
if (planFilename) {
|
|
332
|
-
return `Load the plan at .cortex/plans/${planFilename} and implement all tasks listed in it. Follow the plan's technical approach and phases.`;
|
|
333
|
-
}
|
|
334
|
-
return "Check for plans in .cortex/plans/ and begin implementation. If no plan exists, analyze the codebase and suggest next steps.";
|
|
335
|
-
}
|
|
336
|
-
// ─── Mode A: New Terminal Tab (via driver system) ────────────────────────────
|
|
337
|
-
async function launchTerminalTab(client, worktreePath, branchName, opencodeBin, agent, prompt) {
|
|
338
|
-
/** Fire a toast notification for terminal launch results. */
|
|
339
|
-
const notify = async (message, variant = "success") => {
|
|
340
|
-
try {
|
|
341
|
-
await client.tui.showToast({
|
|
342
|
-
body: {
|
|
343
|
-
title: `Terminal: ${branchName}`,
|
|
344
|
-
message,
|
|
345
|
-
variant,
|
|
346
|
-
duration: variant === "error" ? 8000 : 4000,
|
|
347
|
-
},
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
catch {
|
|
351
|
-
// Toast failure is non-fatal
|
|
352
|
-
}
|
|
353
|
-
};
|
|
354
|
-
// KEY FIX: Use multi-strategy terminal detection that NEVER returns IDE drivers.
|
|
355
|
-
// This ensures "Open in terminal tab" always opens in the user's actual terminal
|
|
356
|
-
// emulator (iTerm2, Ghostty, kitty, etc.), not in a new IDE window.
|
|
357
|
-
const detection = await detectTerminalDriver(worktreePath);
|
|
358
|
-
const driver = detection.driver;
|
|
359
|
-
const opts = {
|
|
360
|
-
worktreePath,
|
|
361
|
-
opencodeBin,
|
|
362
|
-
agent,
|
|
363
|
-
prompt,
|
|
364
|
-
branchName,
|
|
365
|
-
};
|
|
366
|
-
try {
|
|
367
|
-
const result = await driver.openTab(opts);
|
|
368
|
-
// Persist session for later cleanup (e.g., worktree_remove)
|
|
369
|
-
const session = {
|
|
370
|
-
terminal: driver.name,
|
|
371
|
-
platform: process.platform,
|
|
372
|
-
mode: "terminal",
|
|
373
|
-
branch: branchName,
|
|
374
|
-
agent,
|
|
375
|
-
worktreePath,
|
|
376
|
-
startedAt: new Date().toISOString(),
|
|
377
|
-
...result,
|
|
378
|
-
};
|
|
379
|
-
writeSession(worktreePath, session);
|
|
380
|
-
const strategyNote = detection.strategy !== "env"
|
|
381
|
-
? ` (detected via ${detection.strategy})`
|
|
382
|
-
: "";
|
|
383
|
-
await notify(`Opened ${driver.name} tab with agent '${agent}'${strategyNote}`);
|
|
384
|
-
return `✓ Opened ${driver.name} tab in worktree\n\nBranch: ${branchName}\nTerminal: ${driver.name}\nDetection: ${detection.strategy}${detection.detail ? ` — ${detection.detail}` : ""}`;
|
|
385
|
-
}
|
|
386
|
-
catch (error) {
|
|
387
|
-
await notify(`Could not open terminal: ${error.message || error}`, "error");
|
|
388
|
-
// Escape all user-controlled inputs in the manual command to prevent injection
|
|
389
|
-
const safePath = shellEscape(worktreePath);
|
|
390
|
-
const safeBin = shellEscape(opencodeBin);
|
|
391
|
-
const safeAgent = shellEscape(agent);
|
|
392
|
-
const innerCmd = `cd "${safePath}" && "${safeBin}" --agent "${safeAgent}"`;
|
|
393
|
-
return `✗ Could not open terminal (${driver.name}, detected via ${detection.strategy}). Manual command:\n ${innerCmd}`;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
// ─── Mode B: In-App PTY ─────────────────────────────────────────────────────
|
|
397
|
-
async function launchPty(client, worktreePath, branchName, opencodeBin, agent, prompt) {
|
|
398
|
-
try {
|
|
399
|
-
const response = await client.pty.create({
|
|
400
|
-
body: {
|
|
401
|
-
command: opencodeBin,
|
|
402
|
-
args: ["--agent", agent, "--prompt", prompt],
|
|
403
|
-
cwd: worktreePath,
|
|
404
|
-
title: `Worktree: ${branchName}`,
|
|
405
|
-
},
|
|
406
|
-
});
|
|
407
|
-
// Capture PTY ID and PID from response for later cleanup
|
|
408
|
-
const ptyId = response.data?.id;
|
|
409
|
-
const ptyPid = response.data?.pid;
|
|
410
|
-
// Persist session for cleanup on worktree_remove
|
|
411
|
-
writeSession(worktreePath, {
|
|
412
|
-
terminal: "pty",
|
|
413
|
-
platform: process.platform,
|
|
414
|
-
mode: "pty",
|
|
415
|
-
ptyId: ptyId ?? undefined,
|
|
416
|
-
pid: ptyPid ?? undefined,
|
|
417
|
-
branch: branchName,
|
|
418
|
-
agent,
|
|
419
|
-
worktreePath,
|
|
420
|
-
startedAt: new Date().toISOString(),
|
|
421
|
-
});
|
|
422
|
-
// Show toast for PTY launch
|
|
423
|
-
try {
|
|
424
|
-
await client.tui.showToast({
|
|
425
|
-
body: {
|
|
426
|
-
title: `PTY: ${branchName}`,
|
|
427
|
-
message: `Created in-app session with agent '${agent}'`,
|
|
428
|
-
variant: "success",
|
|
429
|
-
duration: 4000,
|
|
430
|
-
},
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
catch {
|
|
434
|
-
// Toast failure is non-fatal
|
|
435
|
-
}
|
|
436
|
-
return `✓ Created in-app PTY session for worktree
|
|
437
|
-
|
|
438
|
-
Branch: ${branchName}
|
|
439
|
-
Title: "Worktree: ${branchName}"
|
|
440
|
-
|
|
441
|
-
The PTY is running OpenCode with agent '${agent}' in the worktree.
|
|
442
|
-
Switch to it using OpenCode's terminal panel.`;
|
|
443
|
-
}
|
|
444
|
-
catch (error) {
|
|
445
|
-
try {
|
|
446
|
-
await client.tui.showToast({
|
|
447
|
-
body: {
|
|
448
|
-
title: `PTY: ${branchName}`,
|
|
449
|
-
message: `Failed to create session: ${error.message || error}`,
|
|
450
|
-
variant: "error",
|
|
451
|
-
duration: 8000,
|
|
452
|
-
},
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
catch {
|
|
456
|
-
// Toast failure is non-fatal
|
|
457
|
-
}
|
|
458
|
-
return `✗ Failed to create PTY session: ${error.message || error}
|
|
459
|
-
|
|
460
|
-
Falling back to manual command:
|
|
461
|
-
cd "${worktreePath}" && "${opencodeBin}" --agent ${agent}`;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
// ─── Mode C: Background Session ─────────────────────────────────────────────
|
|
465
|
-
async function launchBackground(client, worktreePath, branchName, opencodeBin, agent, prompt) {
|
|
466
|
-
// Spawn opencode run as a detached background process
|
|
467
|
-
const proc = shellSpawn(opencodeBin, ["run", "--agent", agent, prompt], {
|
|
468
|
-
cwd: worktreePath,
|
|
469
|
-
stdio: "pipe",
|
|
470
|
-
env: {
|
|
471
|
-
...process.env,
|
|
472
|
-
HOME: process.env.HOME || "",
|
|
473
|
-
PATH: process.env.PATH || "",
|
|
474
|
-
},
|
|
475
|
-
});
|
|
476
|
-
// Save PID for tracking (legacy format)
|
|
477
|
-
const cortexDir = path.join(worktreePath, ".cortex");
|
|
478
|
-
if (!fs.existsSync(cortexDir)) {
|
|
479
|
-
fs.mkdirSync(cortexDir, { recursive: true });
|
|
480
|
-
}
|
|
481
|
-
fs.writeFileSync(path.join(cortexDir, ".background-pid"), JSON.stringify({
|
|
482
|
-
pid: proc.pid,
|
|
483
|
-
branch: branchName,
|
|
484
|
-
agent,
|
|
485
|
-
startedAt: new Date().toISOString(),
|
|
486
|
-
}));
|
|
487
|
-
// Also write unified terminal session for cleanup on worktree_remove
|
|
488
|
-
writeSession(worktreePath, {
|
|
489
|
-
terminal: "background",
|
|
490
|
-
platform: process.platform,
|
|
491
|
-
mode: "background",
|
|
492
|
-
pid: proc.pid ?? undefined,
|
|
493
|
-
branch: branchName,
|
|
494
|
-
agent,
|
|
495
|
-
worktreePath,
|
|
496
|
-
startedAt: new Date().toISOString(),
|
|
497
|
-
});
|
|
498
|
-
// Show initial toast
|
|
499
|
-
try {
|
|
500
|
-
await client.tui.showToast({
|
|
501
|
-
body: {
|
|
502
|
-
title: `Background: ${branchName}`,
|
|
503
|
-
message: `Started background implementation with agent '${agent}'`,
|
|
504
|
-
variant: "info",
|
|
505
|
-
duration: 5000,
|
|
506
|
-
},
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
catch {
|
|
510
|
-
// Toast failure is non-fatal
|
|
511
|
-
}
|
|
512
|
-
// Monitor completion in background (fire-and-forget)
|
|
513
|
-
monitorBackgroundProcess(proc, client, branchName, worktreePath);
|
|
514
|
-
return `✓ Launched background implementation
|
|
515
|
-
|
|
516
|
-
Branch: ${branchName}
|
|
517
|
-
PID: ${proc.pid}
|
|
518
|
-
Agent: ${agent}
|
|
519
|
-
Working in: ${worktreePath}
|
|
520
|
-
|
|
521
|
-
The AI is implementing in the background. You'll get a toast notification
|
|
522
|
-
when it completes or fails.
|
|
523
|
-
|
|
524
|
-
PID tracking file: ${path.join(cortexDir, ".background-pid")}
|
|
525
|
-
|
|
526
|
-
To check worktree status later:
|
|
527
|
-
git -C "${worktreePath}" status
|
|
528
|
-
git -C "${worktreePath}" log --oneline -5`;
|
|
529
|
-
}
|
|
530
|
-
/**
|
|
531
|
-
* Monitor a background process and notify via toast on completion.
|
|
532
|
-
* This runs asynchronously — does not block the tool response.
|
|
533
|
-
*/
|
|
534
|
-
async function monitorBackgroundProcess(proc, client, branchName, worktreePath) {
|
|
535
|
-
try {
|
|
536
|
-
// Wait for the process to exit
|
|
537
|
-
const exitCode = await new Promise((resolve) => {
|
|
538
|
-
proc.on("exit", (code) => resolve(code ?? 1));
|
|
539
|
-
proc.on("error", () => resolve(1));
|
|
540
|
-
});
|
|
541
|
-
// Clean up PID file
|
|
542
|
-
const pidFile = path.join(worktreePath, ".cortex", ".background-pid");
|
|
543
|
-
if (fs.existsSync(pidFile)) {
|
|
544
|
-
fs.unlinkSync(pidFile);
|
|
545
|
-
}
|
|
546
|
-
if (exitCode === 0) {
|
|
547
|
-
await client.tui.showToast({
|
|
548
|
-
body: {
|
|
549
|
-
title: `Background: ${branchName}`,
|
|
550
|
-
message: "Implementation complete! Check the worktree for changes.",
|
|
551
|
-
variant: "success",
|
|
552
|
-
duration: 10000,
|
|
553
|
-
},
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
else {
|
|
557
|
-
await client.tui.showToast({
|
|
558
|
-
body: {
|
|
559
|
-
title: `Background: ${branchName}`,
|
|
560
|
-
message: `Process exited with code ${exitCode}. Check the worktree.`,
|
|
561
|
-
variant: "warning",
|
|
562
|
-
duration: 10000,
|
|
563
|
-
},
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
catch (error) {
|
|
568
|
-
try {
|
|
569
|
-
await client.tui.showToast({
|
|
570
|
-
body: {
|
|
571
|
-
title: `Background: ${branchName}`,
|
|
572
|
-
message: `Error: ${error.message || "Process monitoring failed"}`,
|
|
573
|
-
variant: "error",
|
|
574
|
-
duration: 10000,
|
|
575
|
-
},
|
|
576
|
-
});
|
|
577
|
-
}
|
|
578
|
-
catch {
|
|
579
|
-
// If toast fails too, nothing we can do
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
// ─── worktree_launch Factory ─────────────────────────────────────────────────
|
|
584
|
-
/**
|
|
585
|
-
* Factory function that creates the worktree_launch tool with access
|
|
586
|
-
* to the OpenCode client (for PTY and toast) and shell.
|
|
587
|
-
*
|
|
588
|
-
* This uses a closure to capture `client` and `shell` since ToolContext
|
|
589
|
-
* does not provide access to the OpenCode client API.
|
|
590
|
-
*/
|
|
591
|
-
export function createLaunch(client, shell) {
|
|
592
|
-
return tool({
|
|
593
|
-
description: "Launch an OpenCode session in an existing worktree. Supports three modes: " +
|
|
594
|
-
"'terminal' opens a new terminal tab, 'pty' creates an in-app PTY session, " +
|
|
595
|
-
"'background' runs implementation headlessly with progress notifications.",
|
|
596
|
-
args: {
|
|
597
|
-
name: tool.schema
|
|
598
|
-
.string()
|
|
599
|
-
.describe("Worktree name (must already exist — use worktree_create first)"),
|
|
600
|
-
mode: tool.schema
|
|
601
|
-
.enum(["terminal", "pty", "background"])
|
|
602
|
-
.describe("Launch mode: 'terminal' = new terminal tab, 'pty' = in-app PTY session, " +
|
|
603
|
-
"'background' = headless execution with toast notifications"),
|
|
604
|
-
plan: tool.schema
|
|
605
|
-
.string()
|
|
606
|
-
.optional()
|
|
607
|
-
.describe("Plan filename to propagate into the worktree (e.g., '2026-02-22-feature-auth.md')"),
|
|
608
|
-
agent: tool.schema
|
|
609
|
-
.string()
|
|
610
|
-
.optional()
|
|
611
|
-
.describe("Agent to use in the new session (default: 'implement')"),
|
|
612
|
-
prompt: tool.schema
|
|
613
|
-
.string()
|
|
614
|
-
.optional()
|
|
615
|
-
.describe("Custom prompt for the new session (auto-generated from plan if omitted)"),
|
|
616
|
-
},
|
|
617
|
-
async execute(args, context) {
|
|
618
|
-
const { name, mode, plan: planFilename, agent = "implement", prompt: customPrompt, } = args;
|
|
619
|
-
const worktreePath = path.join(context.worktree, WORKTREE_ROOT, name);
|
|
620
|
-
const absoluteWorktreePath = path.resolve(worktreePath);
|
|
621
|
-
// ── Validate worktree exists ───────────────────────────────
|
|
622
|
-
if (!fs.existsSync(absoluteWorktreePath)) {
|
|
623
|
-
return `✗ Error: Worktree not found at ${absoluteWorktreePath}
|
|
624
|
-
|
|
625
|
-
Use worktree_create to create it first, then worktree_launch to start working in it.
|
|
626
|
-
Use worktree_list to see existing worktrees.`;
|
|
627
|
-
}
|
|
628
|
-
// ── Find opencode binary ───────────────────────────────────
|
|
629
|
-
const opencodeBin = await findOpencodeBinary();
|
|
630
|
-
if (!opencodeBin) {
|
|
631
|
-
return `✗ Error: Could not find the 'opencode' binary.
|
|
632
|
-
|
|
633
|
-
Checked:
|
|
634
|
-
- ~/.opencode/bin/opencode
|
|
635
|
-
- $PATH (via 'which opencode')
|
|
636
|
-
|
|
637
|
-
Install OpenCode or ensure it's in your PATH.`;
|
|
638
|
-
}
|
|
639
|
-
// ── Detect branch name ─────────────────────────────────────
|
|
640
|
-
let branchName = name;
|
|
641
|
-
try {
|
|
642
|
-
const { stdout } = await git(absoluteWorktreePath, "branch", "--show-current");
|
|
643
|
-
if (stdout.trim())
|
|
644
|
-
branchName = stdout.trim();
|
|
645
|
-
}
|
|
646
|
-
catch {
|
|
647
|
-
// Use worktree name as fallback
|
|
648
|
-
}
|
|
649
|
-
// ── Propagate plan to worktree ─────────────────────────────
|
|
650
|
-
let planInfo = "";
|
|
651
|
-
if (planFilename || fs.existsSync(path.join(context.worktree, ".cortex", "plans"))) {
|
|
652
|
-
const result = propagatePlan({
|
|
653
|
-
sourceWorktree: context.worktree,
|
|
654
|
-
targetWorktree: absoluteWorktreePath,
|
|
655
|
-
planFilename,
|
|
656
|
-
});
|
|
657
|
-
if (result.copied.length > 0) {
|
|
658
|
-
planInfo = `\nPlans propagated: ${result.copied.join(", ")}`;
|
|
659
|
-
if (result.initialized) {
|
|
660
|
-
planInfo += " (.cortex initialized in worktree)";
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
// ── Build prompt ───────────────────────────────────────────
|
|
665
|
-
const launchPrompt = buildLaunchPrompt(planFilename, customPrompt);
|
|
666
|
-
// ── Launch based on mode ───────────────────────────────────
|
|
667
|
-
let launchResult;
|
|
668
|
-
switch (mode) {
|
|
669
|
-
case "terminal":
|
|
670
|
-
launchResult = await launchTerminalTab(client, absoluteWorktreePath, branchName, opencodeBin, agent, launchPrompt);
|
|
671
|
-
break;
|
|
672
|
-
case "pty":
|
|
673
|
-
launchResult = await launchPty(client, absoluteWorktreePath, branchName, opencodeBin, agent, launchPrompt);
|
|
674
|
-
break;
|
|
675
|
-
case "background":
|
|
676
|
-
launchResult = await launchBackground(client, absoluteWorktreePath, branchName, opencodeBin, agent, launchPrompt);
|
|
677
|
-
break;
|
|
678
|
-
default:
|
|
679
|
-
launchResult = `✗ Unknown mode: ${mode}`;
|
|
680
|
-
}
|
|
681
|
-
return `${launchResult}${planInfo}`;
|
|
682
|
-
},
|
|
683
|
-
});
|
|
684
|
-
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Change Scope Detection
|
|
3
|
+
*
|
|
4
|
+
* Categorizes changed files by risk level to determine which sub-agents
|
|
5
|
+
* should be triggered during the quality gate. Avoids wasting tokens on
|
|
6
|
+
* trivial changes while ensuring high-risk changes get full coverage.
|
|
7
|
+
*/
|
|
8
|
+
export type ChangeScope = "trivial" | "low" | "standard" | "high";
|
|
9
|
+
export interface ChangeScopeResult {
|
|
10
|
+
/** Overall risk classification */
|
|
11
|
+
scope: ChangeScope;
|
|
12
|
+
/** Human-readable rationale for the classification */
|
|
13
|
+
rationale: string;
|
|
14
|
+
/** Which sub-agents should be launched based on the scope */
|
|
15
|
+
agents: ScopedAgents;
|
|
16
|
+
}
|
|
17
|
+
export interface ScopedAgents {
|
|
18
|
+
testing: boolean;
|
|
19
|
+
security: boolean;
|
|
20
|
+
audit: boolean;
|
|
21
|
+
devops: boolean;
|
|
22
|
+
perf: boolean;
|
|
23
|
+
docsWriter: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Classify a set of changed files into a risk scope and determine
|
|
27
|
+
* which sub-agents should be triggered.
|
|
28
|
+
*
|
|
29
|
+
* @param changedFiles - Array of file paths that were created or modified
|
|
30
|
+
* @returns Classification result with scope, rationale, and agent triggers
|
|
31
|
+
*/
|
|
32
|
+
export declare function classifyChangeScope(changedFiles: string[]): ChangeScopeResult;
|
|
33
|
+
//# sourceMappingURL=change-scope.d.ts.map
|