cortex-agents 2.3.1 → 3.4.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.
- package/.opencode/agents/{plan.md → architect.md} +69 -15
- package/.opencode/agents/{fullstack.md → crosslayer.md} +2 -2
- package/.opencode/agents/{debug.md → fix.md} +11 -9
- package/.opencode/agents/{security.md → guard.md} +1 -1
- package/.opencode/agents/{build.md → implement.md} +121 -22
- package/.opencode/agents/{testing.md → qa.md} +1 -1
- package/.opencode/agents/{devops.md → ship.md} +1 -1
- package/README.md +107 -31
- package/dist/cli.js +87 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +47 -7
- package/dist/registry.d.ts +8 -3
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +16 -2
- package/dist/tools/cortex.d.ts +2 -2
- package/dist/tools/cortex.js +7 -7
- package/dist/tools/environment.d.ts +31 -0
- package/dist/tools/environment.d.ts.map +1 -0
- package/dist/tools/environment.js +93 -0
- package/dist/tools/github.d.ts +42 -0
- package/dist/tools/github.d.ts.map +1 -0
- package/dist/tools/github.js +200 -0
- package/dist/tools/repl.d.ts +50 -0
- package/dist/tools/repl.d.ts.map +1 -0
- package/dist/tools/repl.js +240 -0
- package/dist/tools/task.d.ts +2 -0
- package/dist/tools/task.d.ts.map +1 -1
- package/dist/tools/task.js +25 -30
- package/dist/tools/worktree.d.ts.map +1 -1
- package/dist/tools/worktree.js +22 -11
- package/dist/utils/github.d.ts +104 -0
- package/dist/utils/github.d.ts.map +1 -0
- package/dist/utils/github.js +243 -0
- package/dist/utils/ide.d.ts +76 -0
- package/dist/utils/ide.d.ts.map +1 -0
- package/dist/utils/ide.js +307 -0
- package/dist/utils/plan-extract.d.ts +7 -0
- package/dist/utils/plan-extract.d.ts.map +1 -1
- package/dist/utils/plan-extract.js +25 -1
- package/dist/utils/repl.d.ts +114 -0
- package/dist/utils/repl.d.ts.map +1 -0
- package/dist/utils/repl.js +434 -0
- package/dist/utils/terminal.d.ts +53 -1
- package/dist/utils/terminal.d.ts.map +1 -1
- package/dist/utils/terminal.js +642 -5
- package/package.json +1 -1
- package/dist/plugin.d.ts +0 -1
- package/dist/plugin.d.ts.map +0 -1
- package/dist/plugin.js +0 -4
- /package/.opencode/agents/{review.md → audit.md} +0 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REPL Loop Tools
|
|
3
|
+
*
|
|
4
|
+
* Four tools for the implement agent's iterative task-by-task development loop:
|
|
5
|
+
* - repl_init — Initialize a loop from a plan
|
|
6
|
+
* - repl_status — Get current progress and next task
|
|
7
|
+
* - repl_report — Report task outcome (pass/fail/skip)
|
|
8
|
+
* - repl_summary — Generate markdown summary for PR body
|
|
9
|
+
*/
|
|
10
|
+
import { tool } from "@opencode-ai/plugin";
|
|
11
|
+
import * as fs from "fs";
|
|
12
|
+
import * as path from "path";
|
|
13
|
+
import { parseTasksFromPlan, detectCommands, readReplState, writeReplState, getNextTask, getCurrentTask, isLoopComplete, formatProgress, formatSummary, } from "../utils/repl.js";
|
|
14
|
+
const CORTEX_DIR = ".cortex";
|
|
15
|
+
const PLANS_DIR = "plans";
|
|
16
|
+
// ─── repl_init ───────────────────────────────────────────────────────────────
|
|
17
|
+
export const init = tool({
|
|
18
|
+
description: "Initialize a REPL implementation loop from a plan. Parses plan tasks, " +
|
|
19
|
+
"auto-detects build/test commands, and creates .cortex/repl-state.json " +
|
|
20
|
+
"for tracking progress through each task iteratively.",
|
|
21
|
+
args: {
|
|
22
|
+
planFilename: tool.schema
|
|
23
|
+
.string()
|
|
24
|
+
.describe("Plan filename from .cortex/plans/ to load tasks from"),
|
|
25
|
+
buildCommand: tool.schema
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Override auto-detected build command (e.g., 'npm run build')"),
|
|
29
|
+
testCommand: tool.schema
|
|
30
|
+
.string()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("Override auto-detected test command (e.g., 'npm test')"),
|
|
33
|
+
maxRetries: tool.schema
|
|
34
|
+
.number()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("Max retries per failing task before escalating to user (default: 3)"),
|
|
37
|
+
},
|
|
38
|
+
async execute(args, context) {
|
|
39
|
+
const { planFilename, buildCommand, testCommand, maxRetries = 3 } = args;
|
|
40
|
+
const cwd = context.worktree;
|
|
41
|
+
// 1. Validate plan filename
|
|
42
|
+
if (!planFilename || planFilename === "." || planFilename === "..") {
|
|
43
|
+
return `\u2717 Error: Invalid plan filename.`;
|
|
44
|
+
}
|
|
45
|
+
const plansDir = path.join(cwd, CORTEX_DIR, PLANS_DIR);
|
|
46
|
+
const planPath = path.resolve(plansDir, planFilename);
|
|
47
|
+
const resolvedPlansDir = path.resolve(plansDir);
|
|
48
|
+
// Prevent path traversal — resolved path must be strictly inside plans dir
|
|
49
|
+
if (!planPath.startsWith(resolvedPlansDir + path.sep)) {
|
|
50
|
+
return `\u2717 Error: Invalid plan filename.`;
|
|
51
|
+
}
|
|
52
|
+
if (!fs.existsSync(planPath)) {
|
|
53
|
+
return `\u2717 Error: Plan not found: ${planFilename}\n\nUse plan_list to see available plans.`;
|
|
54
|
+
}
|
|
55
|
+
const planContent = fs.readFileSync(planPath, "utf-8");
|
|
56
|
+
// 2. Parse tasks from plan
|
|
57
|
+
const taskDescriptions = parseTasksFromPlan(planContent);
|
|
58
|
+
if (taskDescriptions.length === 0) {
|
|
59
|
+
return `\u2717 Error: No tasks found in plan: ${planFilename}\n\nThe plan must contain unchecked checkbox items (- [ ] ...) in a ## Tasks section.`;
|
|
60
|
+
}
|
|
61
|
+
// 3. Auto-detect commands (or use overrides)
|
|
62
|
+
const detected = await detectCommands(cwd);
|
|
63
|
+
const finalBuild = buildCommand ?? detected.buildCommand;
|
|
64
|
+
const finalTest = testCommand ?? detected.testCommand;
|
|
65
|
+
// 4. Build initial state
|
|
66
|
+
const tasks = taskDescriptions.map((desc, i) => ({
|
|
67
|
+
index: i,
|
|
68
|
+
description: desc,
|
|
69
|
+
status: "pending",
|
|
70
|
+
retries: 0,
|
|
71
|
+
iterations: [],
|
|
72
|
+
}));
|
|
73
|
+
const state = {
|
|
74
|
+
planFilename,
|
|
75
|
+
startedAt: new Date().toISOString(),
|
|
76
|
+
buildCommand: finalBuild,
|
|
77
|
+
testCommand: finalTest,
|
|
78
|
+
lintCommand: detected.lintCommand,
|
|
79
|
+
maxRetries,
|
|
80
|
+
currentTaskIndex: -1,
|
|
81
|
+
tasks,
|
|
82
|
+
};
|
|
83
|
+
// 5. Write state
|
|
84
|
+
writeReplState(cwd, state);
|
|
85
|
+
// 6. Format output
|
|
86
|
+
const cmdInfo = detected.detected
|
|
87
|
+
? `Auto-detected (${detected.framework})`
|
|
88
|
+
: "Not detected \u2014 provide overrides if needed";
|
|
89
|
+
return `\u2713 REPL loop initialized
|
|
90
|
+
|
|
91
|
+
Plan: ${planFilename}
|
|
92
|
+
Tasks: ${tasks.length}
|
|
93
|
+
Detection: ${cmdInfo}
|
|
94
|
+
|
|
95
|
+
Build: ${finalBuild || "(none)"}
|
|
96
|
+
Test: ${finalTest || "(none)"}
|
|
97
|
+
${detected.lintCommand ? `Lint: ${detected.lintCommand}` : ""}
|
|
98
|
+
Max retries: ${maxRetries}
|
|
99
|
+
|
|
100
|
+
First task (#1):
|
|
101
|
+
"${tasks[0].description}"
|
|
102
|
+
|
|
103
|
+
Run \`repl_status\` to begin, then implement the task and run build/tests.`;
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
// ─── repl_status ─────────────────────────────────────────────────────────────
|
|
107
|
+
export const status = tool({
|
|
108
|
+
description: "Get the current REPL loop progress \u2014 which task is active, " +
|
|
109
|
+
"what\u2019s been completed, retry counts, and detected build/test commands. " +
|
|
110
|
+
"Call this to decide what to implement next.",
|
|
111
|
+
args: {},
|
|
112
|
+
async execute(args, context) {
|
|
113
|
+
const state = readReplState(context.worktree);
|
|
114
|
+
if (!state) {
|
|
115
|
+
return `\u2717 No REPL loop active.\n\nRun repl_init with a plan filename to start a loop.`;
|
|
116
|
+
}
|
|
117
|
+
// Auto-advance: if no task is in_progress, promote the next pending task
|
|
118
|
+
const current = getCurrentTask(state);
|
|
119
|
+
if (!current && !isLoopComplete(state)) {
|
|
120
|
+
const next = getNextTask(state);
|
|
121
|
+
if (next) {
|
|
122
|
+
next.status = "in_progress";
|
|
123
|
+
state.currentTaskIndex = next.index;
|
|
124
|
+
writeReplState(context.worktree, state);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return `\u2713 REPL Loop Status\n\n${formatProgress(state)}`;
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
// ─── repl_report ─────────────────────────────────────────────────────────────
|
|
131
|
+
export const report = tool({
|
|
132
|
+
description: "Report the outcome of the current task iteration. " +
|
|
133
|
+
"After implementing a task and running build/tests, report whether it passed, " +
|
|
134
|
+
"failed, or should be skipped. The loop will auto-advance on pass, " +
|
|
135
|
+
"retry on fail (up to max), or escalate to user when retries exhausted.",
|
|
136
|
+
args: {
|
|
137
|
+
result: tool.schema
|
|
138
|
+
.enum(["pass", "fail", "skip"])
|
|
139
|
+
.describe("Task result: 'pass' (build+tests green), 'fail' (something broke), 'skip' (defer task)"),
|
|
140
|
+
detail: tool.schema
|
|
141
|
+
.string()
|
|
142
|
+
.describe("Result details: test output summary, error message, or skip reason"),
|
|
143
|
+
},
|
|
144
|
+
async execute(args, context) {
|
|
145
|
+
const { result, detail } = args;
|
|
146
|
+
const state = readReplState(context.worktree);
|
|
147
|
+
if (!state) {
|
|
148
|
+
return `\u2717 No REPL loop active. Run repl_init first.`;
|
|
149
|
+
}
|
|
150
|
+
// Find the current in_progress task
|
|
151
|
+
const current = getCurrentTask(state);
|
|
152
|
+
if (!current) {
|
|
153
|
+
// Try to find the task at currentTaskIndex
|
|
154
|
+
if (state.currentTaskIndex >= 0 && state.currentTaskIndex < state.tasks.length) {
|
|
155
|
+
const task = state.tasks[state.currentTaskIndex];
|
|
156
|
+
if (task.status === "pending") {
|
|
157
|
+
task.status = "in_progress";
|
|
158
|
+
}
|
|
159
|
+
return processReport(state, task, result, detail, context.worktree);
|
|
160
|
+
}
|
|
161
|
+
return `\u2717 No task is currently in progress.\n\nRun repl_status to advance to the next task.`;
|
|
162
|
+
}
|
|
163
|
+
return processReport(state, current, result, detail, context.worktree);
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
/**
|
|
167
|
+
* Process a report for a task and update state.
|
|
168
|
+
*/
|
|
169
|
+
function processReport(state, task, result, detail, cwd) {
|
|
170
|
+
// Record iteration
|
|
171
|
+
task.iterations.push({
|
|
172
|
+
at: new Date().toISOString(),
|
|
173
|
+
result,
|
|
174
|
+
detail: detail.substring(0, 2000), // Cap detail length
|
|
175
|
+
});
|
|
176
|
+
const taskNum = task.index + 1;
|
|
177
|
+
const taskDesc = task.description;
|
|
178
|
+
let output;
|
|
179
|
+
switch (result) {
|
|
180
|
+
case "pass": {
|
|
181
|
+
task.status = "passed";
|
|
182
|
+
const attempt = task.iterations.length;
|
|
183
|
+
const suffix = attempt === 1 ? "1st" : attempt === 2 ? "2nd" : attempt === 3 ? "3rd" : `${attempt}th`;
|
|
184
|
+
output = `\u2713 Task #${taskNum} PASSED (${suffix} attempt)\n "${taskDesc}"\n Detail: ${detail.substring(0, 200)}`;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
case "fail": {
|
|
188
|
+
task.retries += 1;
|
|
189
|
+
const attempt = task.iterations.length;
|
|
190
|
+
if (task.retries >= state.maxRetries) {
|
|
191
|
+
// Retries exhausted
|
|
192
|
+
task.status = "failed";
|
|
193
|
+
output = `\u2717 Task #${taskNum} FAILED \u2014 retries exhausted (${attempt}/${state.maxRetries} attempts)\n "${taskDesc}"\n Detail: ${detail.substring(0, 200)}\n\n\u2192 ASK THE USER how to proceed. Suggest: fix manually, skip task, or abort loop.`;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
// Retries remaining — stay in_progress
|
|
197
|
+
const remaining = state.maxRetries - task.retries;
|
|
198
|
+
output = `\u26A0 Task #${taskNum} FAILED (attempt ${attempt}/${state.maxRetries})\n "${taskDesc}"\n Detail: ${detail.substring(0, 200)}\n\n\u2192 Fix the issue and run build/tests again. ${remaining} retr${remaining > 1 ? "ies" : "y"} remaining.`;
|
|
199
|
+
// Don't advance — keep task in_progress
|
|
200
|
+
writeReplState(cwd, state);
|
|
201
|
+
return output;
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
case "skip": {
|
|
206
|
+
task.status = "skipped";
|
|
207
|
+
output = `\u2298 Task #${taskNum} SKIPPED\n "${taskDesc}"\n Reason: ${detail.substring(0, 200)}`;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Advance to next task
|
|
212
|
+
const next = getNextTask(state);
|
|
213
|
+
if (next) {
|
|
214
|
+
next.status = "in_progress";
|
|
215
|
+
state.currentTaskIndex = next.index;
|
|
216
|
+
output += `\n\n\u2192 Next: Task #${next.index + 1} "${next.description}"`;
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
// All tasks done
|
|
220
|
+
state.currentTaskIndex = -1;
|
|
221
|
+
state.completedAt = new Date().toISOString();
|
|
222
|
+
output += "\n\n\u2713 All tasks complete. Run repl_summary to generate the results report, then proceed to the quality gate (Step 7).";
|
|
223
|
+
}
|
|
224
|
+
writeReplState(cwd, state);
|
|
225
|
+
return output;
|
|
226
|
+
}
|
|
227
|
+
// ─── repl_summary ────────────────────────────────────────────────────────────
|
|
228
|
+
export const summary = tool({
|
|
229
|
+
description: "Generate a formatted summary of the REPL loop results for inclusion in " +
|
|
230
|
+
"the quality gate report and PR body. Call after all tasks are complete " +
|
|
231
|
+
"or when the loop is terminated.",
|
|
232
|
+
args: {},
|
|
233
|
+
async execute(args, context) {
|
|
234
|
+
const state = readReplState(context.worktree);
|
|
235
|
+
if (!state) {
|
|
236
|
+
return `\u2717 No REPL loop data found.\n\nRun repl_init to start a loop, or this may have been cleaned up already.`;
|
|
237
|
+
}
|
|
238
|
+
return formatSummary(state);
|
|
239
|
+
},
|
|
240
|
+
});
|
package/dist/tools/task.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare const finalize: {
|
|
|
7
7
|
baseBranch: import("zod").ZodOptional<import("zod").ZodString>;
|
|
8
8
|
planFilename: import("zod").ZodOptional<import("zod").ZodString>;
|
|
9
9
|
draft: import("zod").ZodOptional<import("zod").ZodBoolean>;
|
|
10
|
+
issueRefs: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodNumber>>;
|
|
10
11
|
};
|
|
11
12
|
execute(args: {
|
|
12
13
|
commitMessage: string;
|
|
@@ -15,6 +16,7 @@ export declare const finalize: {
|
|
|
15
16
|
baseBranch?: string | undefined;
|
|
16
17
|
planFilename?: string | undefined;
|
|
17
18
|
draft?: boolean | undefined;
|
|
19
|
+
issueRefs?: number[] | undefined;
|
|
18
20
|
}, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
19
21
|
};
|
|
20
22
|
//# sourceMappingURL=task.d.ts.map
|
package/dist/tools/task.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task.d.ts","sourceRoot":"","sources":["../../src/tools/task.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"task.d.ts","sourceRoot":"","sources":["../../src/tools/task.ts"],"names":[],"mappings":"AA+EA,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;CA0OnB,CAAC"}
|
package/dist/tools/task.js
CHANGED
|
@@ -2,35 +2,12 @@ import { tool } from "@opencode-ai/plugin";
|
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import { detectWorktreeInfo } from "../utils/worktree-detect.js";
|
|
5
|
-
import { findPlanContent, extractPlanSections, buildPrBodyFromPlan, } from "../utils/plan-extract.js";
|
|
6
|
-
import { git, gh
|
|
5
|
+
import { findPlanContent, extractPlanSections, extractIssueRefs, buildPrBodyFromPlan, } from "../utils/plan-extract.js";
|
|
6
|
+
import { git, gh } from "../utils/shell.js";
|
|
7
|
+
import { checkGhAvailability } from "../utils/github.js";
|
|
7
8
|
const PROTECTED_BRANCHES = ["main", "master", "develop", "production", "staging"];
|
|
8
9
|
const DOCS_DIR = "docs";
|
|
9
10
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
10
|
-
/**
|
|
11
|
-
* Check if `gh` CLI is installed and authenticated.
|
|
12
|
-
*/
|
|
13
|
-
async function checkGhCli(cwd) {
|
|
14
|
-
// Check if gh exists
|
|
15
|
-
const ghPath = await which("gh");
|
|
16
|
-
if (!ghPath) {
|
|
17
|
-
return {
|
|
18
|
-
ok: false,
|
|
19
|
-
error: "GitHub CLI (gh) is not installed. Install it from https://cli.github.com/ and run `gh auth login`.",
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
// Check if authenticated
|
|
23
|
-
try {
|
|
24
|
-
await gh(cwd, "auth", "status");
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return {
|
|
28
|
-
ok: false,
|
|
29
|
-
error: "GitHub CLI is not authenticated. Run `gh auth login` to authenticate.",
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
return { ok: true };
|
|
33
|
-
}
|
|
34
11
|
/**
|
|
35
12
|
* Check if a remote named "origin" is configured.
|
|
36
13
|
*/
|
|
@@ -117,9 +94,13 @@ export const finalize = tool({
|
|
|
117
94
|
.boolean()
|
|
118
95
|
.optional()
|
|
119
96
|
.describe("Create as draft PR (default: false)"),
|
|
97
|
+
issueRefs: tool.schema
|
|
98
|
+
.array(tool.schema.number())
|
|
99
|
+
.optional()
|
|
100
|
+
.describe("GitHub issue numbers to link in PR body (adds 'Closes #N' for each)"),
|
|
120
101
|
},
|
|
121
102
|
async execute(args, context) {
|
|
122
|
-
const { commitMessage, prTitle, prBody: customPrBody, baseBranch: customBaseBranch, planFilename, draft = false, } = args;
|
|
103
|
+
const { commitMessage, prTitle, prBody: customPrBody, baseBranch: customBaseBranch, planFilename, draft = false, issueRefs: explicitIssueRefs, } = args;
|
|
123
104
|
const cwd = context.worktree;
|
|
124
105
|
const output = [];
|
|
125
106
|
const warnings = [];
|
|
@@ -162,9 +143,12 @@ Create a feature/bugfix branch first with branch_create or worktree_create.`;
|
|
|
162
143
|
output.push(`Worktree detected (main tree: ${wtInfo.mainWorktreePath})`);
|
|
163
144
|
}
|
|
164
145
|
// ── 4. Check prerequisites ────────────────────────────────
|
|
165
|
-
const
|
|
166
|
-
if (!
|
|
167
|
-
return
|
|
146
|
+
const ghStatus = await checkGhAvailability(cwd);
|
|
147
|
+
if (!ghStatus.installed) {
|
|
148
|
+
return "✗ GitHub CLI (gh) is not installed. Install it from https://cli.github.com/ and run `gh auth login`.";
|
|
149
|
+
}
|
|
150
|
+
if (!ghStatus.authenticated) {
|
|
151
|
+
return "✗ GitHub CLI is not authenticated. Run `gh auth login` to authenticate.";
|
|
168
152
|
}
|
|
169
153
|
const remoteCheck = await checkRemote(cwd);
|
|
170
154
|
if (!remoteCheck.ok) {
|
|
@@ -218,6 +202,7 @@ All previous steps succeeded (changes committed). Try pushing manually:
|
|
|
218
202
|
}
|
|
219
203
|
// ── 9. Build PR body ──────────────────────────────────────
|
|
220
204
|
let prBodyContent = customPrBody || "";
|
|
205
|
+
let issueRefs = explicitIssueRefs ?? [];
|
|
221
206
|
if (!prBodyContent) {
|
|
222
207
|
// Try to build from plan
|
|
223
208
|
const plan = findPlanContent(cwd, planFilename, branchName);
|
|
@@ -225,6 +210,10 @@ All previous steps succeeded (changes committed). Try pushing manually:
|
|
|
225
210
|
const sections = extractPlanSections(plan.content, plan.filename);
|
|
226
211
|
prBodyContent = buildPrBodyFromPlan(sections);
|
|
227
212
|
output.push(`PR body generated from plan: ${plan.filename}`);
|
|
213
|
+
// Extract issue refs from plan frontmatter if not explicitly provided
|
|
214
|
+
if (issueRefs.length === 0) {
|
|
215
|
+
issueRefs = extractIssueRefs(plan.content);
|
|
216
|
+
}
|
|
228
217
|
}
|
|
229
218
|
else {
|
|
230
219
|
// Fall back to commit log
|
|
@@ -237,6 +226,12 @@ All previous steps succeeded (changes committed). Try pushing manually:
|
|
|
237
226
|
}
|
|
238
227
|
}
|
|
239
228
|
}
|
|
229
|
+
// Append issue closing references to PR body
|
|
230
|
+
if (issueRefs.length > 0) {
|
|
231
|
+
const closingRefs = issueRefs.map((n) => `Closes #${n}`).join("\n");
|
|
232
|
+
prBodyContent += `\n\n## Linked Issues\n\n${closingRefs}`;
|
|
233
|
+
output.push(`Linked issues: ${issueRefs.map((n) => `#${n}`).join(", ")}`);
|
|
234
|
+
}
|
|
240
235
|
// ── 10. Create PR via gh ──────────────────────────────────
|
|
241
236
|
const finalPrTitle = prTitle || commitMessage;
|
|
242
237
|
let prUrl = "";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worktree.d.ts","sourceRoot":"","sources":["../../src/tools/worktree.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAiBvD,KAAK,MAAM,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;AACpC,KAAK,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;AAE9B;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;;;;;;;;;EAuF1C;AAED,eAAO,MAAM,IAAI;;;;CAiCf,CAAC;AAEH;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;EAkI1C;AAED,eAAO,MAAM,IAAI;;;;;;;;CAgDf,CAAC;
|
|
1
|
+
{"version":3,"file":"worktree.d.ts","sourceRoot":"","sources":["../../src/tools/worktree.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAiBvD,KAAK,MAAM,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;AACpC,KAAK,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;AAE9B;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;;;;;;;;;EAuF1C;AAED,eAAO,MAAM,IAAI;;;;CAiCf,CAAC;AAEH;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;EAkI1C;AAED,eAAO,MAAM,IAAI;;;;;;;;CAgDf,CAAC;AA8UH;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;;;;;;;;;;;;;;;;;;;;EAsIxD"}
|
package/dist/tools/worktree.js
CHANGED
|
@@ -2,8 +2,8 @@ import { tool } from "@opencode-ai/plugin";
|
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import { propagatePlan } from "../utils/propagate.js";
|
|
5
|
-
import { git, which, kill, spawn as shellSpawn } from "../utils/shell.js";
|
|
6
|
-
import {
|
|
5
|
+
import { git, which, shellEscape, kill, spawn as shellSpawn } from "../utils/shell.js";
|
|
6
|
+
import { detectTerminalDriver, closeSession, writeSession, readSession, } from "../utils/terminal.js";
|
|
7
7
|
const WORKTREE_ROOT = ".worktrees";
|
|
8
8
|
/**
|
|
9
9
|
* Factory function that creates the worktree_create tool with access
|
|
@@ -171,8 +171,8 @@ Use worktree_list to see existing worktrees.`;
|
|
|
171
171
|
// PTY may already be closed
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
-
else if (session.mode === "terminal") {
|
|
175
|
-
// Close terminal tab via driver
|
|
174
|
+
else if (session.mode === "terminal" || session.mode === "ide") {
|
|
175
|
+
// Close terminal/IDE tab via driver
|
|
176
176
|
closedSession = await closeSession(session);
|
|
177
177
|
}
|
|
178
178
|
else if (session.mode === "background" && session.pid) {
|
|
@@ -351,7 +351,11 @@ async function launchTerminalTab(client, worktreePath, branchName, opencodeBin,
|
|
|
351
351
|
// Toast failure is non-fatal
|
|
352
352
|
}
|
|
353
353
|
};
|
|
354
|
-
|
|
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;
|
|
355
359
|
const opts = {
|
|
356
360
|
worktreePath,
|
|
357
361
|
opencodeBin,
|
|
@@ -373,13 +377,20 @@ async function launchTerminalTab(client, worktreePath, branchName, opencodeBin,
|
|
|
373
377
|
...result,
|
|
374
378
|
};
|
|
375
379
|
writeSession(worktreePath, session);
|
|
376
|
-
|
|
377
|
-
|
|
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}` : ""}`;
|
|
378
385
|
}
|
|
379
386
|
catch (error) {
|
|
380
387
|
await notify(`Could not open terminal: ${error.message || error}`, "error");
|
|
381
|
-
|
|
382
|
-
|
|
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}`;
|
|
383
394
|
}
|
|
384
395
|
}
|
|
385
396
|
// ─── Mode B: In-App PTY ─────────────────────────────────────────────────────
|
|
@@ -597,14 +608,14 @@ export function createLaunch(client, shell) {
|
|
|
597
608
|
agent: tool.schema
|
|
598
609
|
.string()
|
|
599
610
|
.optional()
|
|
600
|
-
.describe("Agent to use in the new session (default: '
|
|
611
|
+
.describe("Agent to use in the new session (default: 'implement')"),
|
|
601
612
|
prompt: tool.schema
|
|
602
613
|
.string()
|
|
603
614
|
.optional()
|
|
604
615
|
.describe("Custom prompt for the new session (auto-generated from plan if omitted)"),
|
|
605
616
|
},
|
|
606
617
|
async execute(args, context) {
|
|
607
|
-
const { name, mode, plan: planFilename, agent = "
|
|
618
|
+
const { name, mode, plan: planFilename, agent = "implement", prompt: customPrompt, } = args;
|
|
608
619
|
const worktreePath = path.join(context.worktree, WORKTREE_ROOT, name);
|
|
609
620
|
const absoluteWorktreePath = path.resolve(worktreePath);
|
|
610
621
|
// ── Validate worktree exists ───────────────────────────────
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export interface GhStatus {
|
|
2
|
+
installed: boolean;
|
|
3
|
+
authenticated: boolean;
|
|
4
|
+
hasRemote: boolean;
|
|
5
|
+
repoOwner?: string;
|
|
6
|
+
repoName?: string;
|
|
7
|
+
projects: {
|
|
8
|
+
id: string;
|
|
9
|
+
number: number;
|
|
10
|
+
title: string;
|
|
11
|
+
}[];
|
|
12
|
+
}
|
|
13
|
+
export interface GitHubIssue {
|
|
14
|
+
number: number;
|
|
15
|
+
title: string;
|
|
16
|
+
state: string;
|
|
17
|
+
labels: string[];
|
|
18
|
+
assignees: string[];
|
|
19
|
+
milestone?: string;
|
|
20
|
+
body: string;
|
|
21
|
+
url: string;
|
|
22
|
+
createdAt: string;
|
|
23
|
+
updatedAt: string;
|
|
24
|
+
}
|
|
25
|
+
export interface GitHubProjectItem {
|
|
26
|
+
id: string;
|
|
27
|
+
title: string;
|
|
28
|
+
type: "ISSUE" | "PULL_REQUEST" | "DRAFT_ISSUE";
|
|
29
|
+
status?: string;
|
|
30
|
+
assignees: string[];
|
|
31
|
+
labels: string[];
|
|
32
|
+
issueNumber?: number;
|
|
33
|
+
url?: string;
|
|
34
|
+
body?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Check full GitHub CLI availability and repo context.
|
|
38
|
+
* Returns a status object with installation, authentication, and repo info.
|
|
39
|
+
*/
|
|
40
|
+
export declare function checkGhAvailability(cwd: string): Promise<GhStatus>;
|
|
41
|
+
/**
|
|
42
|
+
* Parse a git remote URL to extract owner and repo name.
|
|
43
|
+
* Handles HTTPS, SSH, and GitHub CLI formats.
|
|
44
|
+
* Supports both github.com and GitHub Enterprise Server URLs (e.g., github.mycompany.com).
|
|
45
|
+
*
|
|
46
|
+
* The regex requires "github" to appear at a hostname boundary (after `//` or `@`)
|
|
47
|
+
* to prevent false positives like "notgithub.com" or "fakegithub.evil.com".
|
|
48
|
+
*/
|
|
49
|
+
export declare function parseRepoUrl(url: string): {
|
|
50
|
+
owner: string;
|
|
51
|
+
name: string;
|
|
52
|
+
} | null;
|
|
53
|
+
/**
|
|
54
|
+
* Truncate a string to the given length, appending "..." if truncated.
|
|
55
|
+
*/
|
|
56
|
+
export declare function truncate(str: string, maxLen: number): string;
|
|
57
|
+
/**
|
|
58
|
+
* Format a single GitHub issue into a compact list entry.
|
|
59
|
+
* Used when presenting multiple issues for selection.
|
|
60
|
+
*/
|
|
61
|
+
export declare function formatIssueListEntry(issue: GitHubIssue): string;
|
|
62
|
+
/**
|
|
63
|
+
* Format multiple issues into a numbered selection list.
|
|
64
|
+
*/
|
|
65
|
+
export declare function formatIssueList(issues: GitHubIssue[]): string;
|
|
66
|
+
/**
|
|
67
|
+
* Format a GitHub issue into a planning-friendly markdown block.
|
|
68
|
+
* Used by the architect agent to seed plan content from selected issues.
|
|
69
|
+
*/
|
|
70
|
+
export declare function formatIssueForPlan(issue: GitHubIssue): string;
|
|
71
|
+
/**
|
|
72
|
+
* Format a single GitHub Project item into a compact list entry.
|
|
73
|
+
*/
|
|
74
|
+
export declare function formatProjectItemEntry(item: GitHubProjectItem): string;
|
|
75
|
+
/**
|
|
76
|
+
* Format multiple project items into a list.
|
|
77
|
+
*/
|
|
78
|
+
export declare function formatProjectItemList(items: GitHubProjectItem[]): string;
|
|
79
|
+
/**
|
|
80
|
+
* Fetch GitHub projects associated with the repo owner.
|
|
81
|
+
*/
|
|
82
|
+
export declare function fetchProjects(cwd: string, owner: string): Promise<{
|
|
83
|
+
id: string;
|
|
84
|
+
number: number;
|
|
85
|
+
title: string;
|
|
86
|
+
}[]>;
|
|
87
|
+
/**
|
|
88
|
+
* Fetch issues from the current repository using gh CLI.
|
|
89
|
+
*/
|
|
90
|
+
export declare function fetchIssues(cwd: string, options?: {
|
|
91
|
+
state?: string;
|
|
92
|
+
labels?: string;
|
|
93
|
+
milestone?: string;
|
|
94
|
+
assignee?: string;
|
|
95
|
+
limit?: number;
|
|
96
|
+
}): Promise<GitHubIssue[]>;
|
|
97
|
+
/**
|
|
98
|
+
* Fetch project items from a specific GitHub Project.
|
|
99
|
+
*/
|
|
100
|
+
export declare function fetchProjectItems(cwd: string, owner: string, projectNumber: number, options?: {
|
|
101
|
+
status?: string;
|
|
102
|
+
limit?: number;
|
|
103
|
+
}): Promise<GitHubProjectItem[]>;
|
|
104
|
+
//# sourceMappingURL=github.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../../src/utils/github.ts"],"names":[],"mappings":"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"}
|