cortex-agents 3.4.0 → 4.0.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.
Files changed (48) hide show
  1. package/.opencode/agents/architect.md +81 -89
  2. package/.opencode/agents/audit.md +57 -188
  3. package/.opencode/agents/{crosslayer.md → coder.md} +8 -52
  4. package/.opencode/agents/debug.md +151 -0
  5. package/.opencode/agents/devops.md +142 -0
  6. package/.opencode/agents/docs-writer.md +195 -0
  7. package/.opencode/agents/fix.md +118 -189
  8. package/.opencode/agents/implement.md +114 -74
  9. package/.opencode/agents/perf.md +151 -0
  10. package/.opencode/agents/refactor.md +163 -0
  11. package/.opencode/agents/{guard.md → security.md} +20 -85
  12. package/.opencode/agents/testing.md +115 -0
  13. package/.opencode/skills/data-engineering/SKILL.md +221 -0
  14. package/.opencode/skills/monitoring-observability/SKILL.md +251 -0
  15. package/README.md +302 -287
  16. package/dist/cli.js +6 -9
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +26 -28
  19. package/dist/registry.d.ts +4 -4
  20. package/dist/registry.d.ts.map +1 -1
  21. package/dist/registry.js +6 -6
  22. package/dist/tools/branch.d.ts +2 -2
  23. package/dist/tools/docs.d.ts +2 -2
  24. package/dist/tools/github.d.ts +3 -3
  25. package/dist/tools/plan.d.ts +28 -4
  26. package/dist/tools/plan.d.ts.map +1 -1
  27. package/dist/tools/plan.js +232 -4
  28. package/dist/tools/quality-gate.d.ts +28 -0
  29. package/dist/tools/quality-gate.d.ts.map +1 -0
  30. package/dist/tools/quality-gate.js +233 -0
  31. package/dist/tools/repl.d.ts +5 -0
  32. package/dist/tools/repl.d.ts.map +1 -1
  33. package/dist/tools/repl.js +58 -7
  34. package/dist/tools/worktree.d.ts +5 -32
  35. package/dist/tools/worktree.d.ts.map +1 -1
  36. package/dist/tools/worktree.js +75 -458
  37. package/dist/utils/change-scope.d.ts +33 -0
  38. package/dist/utils/change-scope.d.ts.map +1 -0
  39. package/dist/utils/change-scope.js +198 -0
  40. package/dist/utils/plan-extract.d.ts +21 -0
  41. package/dist/utils/plan-extract.d.ts.map +1 -1
  42. package/dist/utils/plan-extract.js +65 -0
  43. package/dist/utils/repl.d.ts +31 -0
  44. package/dist/utils/repl.d.ts.map +1 -1
  45. package/dist/utils/repl.js +126 -13
  46. package/package.json +1 -1
  47. package/.opencode/agents/qa.md +0 -265
  48. package/.opencode/agents/ship.md +0 -249
@@ -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 { propagatePlan } from "../utils/propagate.js";
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 " Error: Not in a git repository. Initialize git first.";
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 `✗ Error: Worktree already exists at ${absoluteWorktreePath}
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 with a new branch
47
- try {
48
- await git(context.worktree, "worktree", "add", "-b", branchName, absoluteWorktreePath);
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
- catch {
51
- // Branch might already exist, try without -b
78
+ else {
79
+ // Create with a new branch
52
80
  try {
53
- await git(context.worktree, "worktree", "add", absoluteWorktreePath, branchName);
81
+ await git(context.worktree, "worktree", "add", "-b", branchName, absoluteWorktreePath);
54
82
  }
55
- catch (error2) {
83
+ catch {
84
+ // Branch might already exist, try without -b
56
85
  try {
57
- await client.tui.showToast({
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
- // Toast failure is non-fatal
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 `✓ Created worktree successfully
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 += `• ${branch}${marker}\n`;
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 `✗ Error listing worktrees: ${error.message || error}`;
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 and PTY cleanup.
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 `✗ Error: Worktree not found at ${absoluteWorktreePath}
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
- // ── Close terminal session BEFORE git removes the directory ──
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 `✗ Error removing worktree: ${error2.message || error2}
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 = `✓ Removed worktree at ${absoluteWorktreePath}`;
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 Deleted branch ${branchName}`;
228
+ output += `\n\u2713 Deleted branch ${branchName}`;
235
229
  }
236
230
  catch (error) {
237
- output += `\n Could not delete branch ${branchName}: ${error.message || error}`;
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${closedInfo} (branch ${branchName} ${deleteBranch ? "deleted" : "kept"})`
249
- : `Removed worktree${closedInfo}`,
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 `✗ Error: Worktree not found at ${absoluteWorktreePath}
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