cortex-agents 2.3.0 → 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} +104 -45
- package/.opencode/agents/audit.md +314 -0
- package/.opencode/agents/crosslayer.md +218 -0
- package/.opencode/agents/{debug.md → fix.md} +75 -46
- package/.opencode/agents/guard.md +202 -0
- package/.opencode/agents/{build.md → implement.md} +151 -107
- package/.opencode/agents/qa.md +265 -0
- package/.opencode/agents/ship.md +249 -0
- package/README.md +119 -31
- package/dist/cli.js +87 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +215 -9
- 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/.opencode/agents/devops.md +0 -176
- package/.opencode/agents/fullstack.md +0 -171
- package/.opencode/agents/security.md +0 -148
- package/.opencode/agents/testing.md +0 -132
- package/dist/plugin.d.ts +0 -1
- package/dist/plugin.d.ts.map +0 -1
- package/dist/plugin.js +0 -4
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REPL Loop Utilities
|
|
3
|
+
*
|
|
4
|
+
* State management, plan task parsing, build/test command auto-detection,
|
|
5
|
+
* and progress formatting for the implement agent's iterative task loop.
|
|
6
|
+
*
|
|
7
|
+
* State is persisted to `.cortex/repl-state.json` so it survives context
|
|
8
|
+
* compaction, session restarts, and agent switches.
|
|
9
|
+
*/
|
|
10
|
+
import * as crypto from "crypto";
|
|
11
|
+
import * as fs from "fs";
|
|
12
|
+
import * as path from "path";
|
|
13
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
14
|
+
const CORTEX_DIR = ".cortex";
|
|
15
|
+
const REPL_STATE_FILE = "repl-state.json";
|
|
16
|
+
// ─── Task Parsing ────────────────────────────────────────────────────────────
|
|
17
|
+
/**
|
|
18
|
+
* Parse plan tasks from plan markdown content.
|
|
19
|
+
*
|
|
20
|
+
* Looks for unchecked checkbox items (`- [ ] ...`) in a `## Tasks` section.
|
|
21
|
+
* Falls back to any unchecked checkboxes anywhere in the document.
|
|
22
|
+
* Strips the `Task N:` prefix if present to get a clean description.
|
|
23
|
+
*/
|
|
24
|
+
export function parseTasksFromPlan(planContent) {
|
|
25
|
+
// Try to find a ## Tasks section first
|
|
26
|
+
const tasksSection = extractTasksSection(planContent);
|
|
27
|
+
const source = tasksSection || planContent;
|
|
28
|
+
const tasks = [];
|
|
29
|
+
const lines = source.split("\n");
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
// Match unchecked checkbox items: - [ ] Description
|
|
32
|
+
const match = line.match(/^[-*]\s*\[\s\]\s+(.+)$/);
|
|
33
|
+
if (match) {
|
|
34
|
+
let description = match[1].trim();
|
|
35
|
+
// Strip "Task N:" prefix if present
|
|
36
|
+
description = description.replace(/^Task\s+\d+\s*:\s*/i, "");
|
|
37
|
+
if (description) {
|
|
38
|
+
tasks.push(description);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return tasks;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Extract the content of the ## Tasks section from plan markdown.
|
|
46
|
+
* Returns null if no Tasks section found.
|
|
47
|
+
*/
|
|
48
|
+
function extractTasksSection(content) {
|
|
49
|
+
const lines = content.split("\n");
|
|
50
|
+
let inTasksSection = false;
|
|
51
|
+
const sectionLines = [];
|
|
52
|
+
for (const line of lines) {
|
|
53
|
+
if (/^##\s+Tasks/i.test(line)) {
|
|
54
|
+
inTasksSection = true;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (inTasksSection) {
|
|
58
|
+
// End of section when we hit another ## heading
|
|
59
|
+
if (/^##\s+/.test(line))
|
|
60
|
+
break;
|
|
61
|
+
sectionLines.push(line);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return sectionLines.length > 0 ? sectionLines.join("\n") : null;
|
|
65
|
+
}
|
|
66
|
+
// ─── Command Auto-Detection ──────────────────────────────────────────────────
|
|
67
|
+
/**
|
|
68
|
+
* Auto-detect build, test, and lint commands from project configuration files.
|
|
69
|
+
*
|
|
70
|
+
* Detection priority:
|
|
71
|
+
* 1. package.json (npm/node projects)
|
|
72
|
+
* 2. Makefile
|
|
73
|
+
* 3. Cargo.toml (Rust)
|
|
74
|
+
* 4. go.mod (Go)
|
|
75
|
+
* 5. pyproject.toml / setup.py (Python)
|
|
76
|
+
* 6. mix.exs (Elixir)
|
|
77
|
+
*/
|
|
78
|
+
export async function detectCommands(cwd) {
|
|
79
|
+
const result = {
|
|
80
|
+
buildCommand: null,
|
|
81
|
+
testCommand: null,
|
|
82
|
+
lintCommand: null,
|
|
83
|
+
framework: "unknown",
|
|
84
|
+
detected: false,
|
|
85
|
+
};
|
|
86
|
+
// 1. Check package.json (most common for this project type)
|
|
87
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
88
|
+
if (fs.existsSync(pkgPath)) {
|
|
89
|
+
try {
|
|
90
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
91
|
+
const scripts = pkg.scripts || {};
|
|
92
|
+
const devDeps = pkg.devDependencies || {};
|
|
93
|
+
const deps = pkg.dependencies || {};
|
|
94
|
+
// Build command
|
|
95
|
+
if (scripts.build) {
|
|
96
|
+
result.buildCommand = "npm run build";
|
|
97
|
+
}
|
|
98
|
+
// Test command — prefer specific runner detection
|
|
99
|
+
if (devDeps.vitest || deps.vitest) {
|
|
100
|
+
result.testCommand = "npx vitest run";
|
|
101
|
+
result.framework = "vitest";
|
|
102
|
+
}
|
|
103
|
+
else if (devDeps.jest || deps.jest) {
|
|
104
|
+
result.testCommand = "npx jest";
|
|
105
|
+
result.framework = "jest";
|
|
106
|
+
}
|
|
107
|
+
else if (devDeps.mocha || deps.mocha) {
|
|
108
|
+
result.testCommand = "npx mocha";
|
|
109
|
+
result.framework = "mocha";
|
|
110
|
+
}
|
|
111
|
+
else if (scripts.test && scripts.test !== 'echo "Error: no test specified" && exit 1') {
|
|
112
|
+
result.testCommand = "npm test";
|
|
113
|
+
result.framework = "npm-test";
|
|
114
|
+
}
|
|
115
|
+
// Lint command
|
|
116
|
+
if (scripts.lint) {
|
|
117
|
+
result.lintCommand = "npm run lint";
|
|
118
|
+
}
|
|
119
|
+
result.detected = !!(result.buildCommand || result.testCommand);
|
|
120
|
+
if (result.detected)
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Malformed package.json — continue to next detector
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// 2. Check Makefile
|
|
128
|
+
const makefilePath = path.join(cwd, "Makefile");
|
|
129
|
+
if (fs.existsSync(makefilePath)) {
|
|
130
|
+
try {
|
|
131
|
+
const makefile = fs.readFileSync(makefilePath, "utf-8");
|
|
132
|
+
if (/^build\s*:/m.test(makefile)) {
|
|
133
|
+
result.buildCommand = "make build";
|
|
134
|
+
}
|
|
135
|
+
if (/^test\s*:/m.test(makefile)) {
|
|
136
|
+
result.testCommand = "make test";
|
|
137
|
+
result.framework = "make";
|
|
138
|
+
}
|
|
139
|
+
if (/^lint\s*:/m.test(makefile)) {
|
|
140
|
+
result.lintCommand = "make lint";
|
|
141
|
+
}
|
|
142
|
+
result.detected = !!(result.buildCommand || result.testCommand);
|
|
143
|
+
if (result.detected)
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Continue to next detector
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// 3. Check Cargo.toml (Rust)
|
|
151
|
+
if (fs.existsSync(path.join(cwd, "Cargo.toml"))) {
|
|
152
|
+
result.buildCommand = "cargo build";
|
|
153
|
+
result.testCommand = "cargo test";
|
|
154
|
+
result.framework = "cargo";
|
|
155
|
+
result.detected = true;
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
// 4. Check go.mod (Go)
|
|
159
|
+
if (fs.existsSync(path.join(cwd, "go.mod"))) {
|
|
160
|
+
result.buildCommand = "go build ./...";
|
|
161
|
+
result.testCommand = "go test ./...";
|
|
162
|
+
result.framework = "go-test";
|
|
163
|
+
result.detected = true;
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
// 5. Check pyproject.toml or setup.py (Python)
|
|
167
|
+
const pyprojectPath = path.join(cwd, "pyproject.toml");
|
|
168
|
+
if (fs.existsSync(pyprojectPath)) {
|
|
169
|
+
try {
|
|
170
|
+
const pyproject = fs.readFileSync(pyprojectPath, "utf-8");
|
|
171
|
+
if (pyproject.includes("pytest")) {
|
|
172
|
+
result.testCommand = "pytest";
|
|
173
|
+
result.framework = "pytest";
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
result.testCommand = "python -m pytest";
|
|
177
|
+
result.framework = "pytest";
|
|
178
|
+
}
|
|
179
|
+
result.detected = true;
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// Continue
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (fs.existsSync(path.join(cwd, "setup.py"))) {
|
|
187
|
+
result.testCommand = "python -m pytest";
|
|
188
|
+
result.framework = "pytest";
|
|
189
|
+
result.detected = true;
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
// 6. Check mix.exs (Elixir)
|
|
193
|
+
if (fs.existsSync(path.join(cwd, "mix.exs"))) {
|
|
194
|
+
result.buildCommand = "mix compile";
|
|
195
|
+
result.testCommand = "mix test";
|
|
196
|
+
result.framework = "ExUnit";
|
|
197
|
+
result.detected = true;
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
// ─── State Management ────────────────────────────────────────────────────────
|
|
203
|
+
/**
|
|
204
|
+
* Get the path to the REPL state file.
|
|
205
|
+
*/
|
|
206
|
+
function statePath(cwd) {
|
|
207
|
+
return path.join(cwd, CORTEX_DIR, REPL_STATE_FILE);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Read the current REPL state from .cortex/repl-state.json.
|
|
211
|
+
* Returns null if no state file exists.
|
|
212
|
+
*/
|
|
213
|
+
export function readReplState(cwd) {
|
|
214
|
+
const filepath = statePath(cwd);
|
|
215
|
+
if (!fs.existsSync(filepath))
|
|
216
|
+
return null;
|
|
217
|
+
try {
|
|
218
|
+
const raw = JSON.parse(fs.readFileSync(filepath, "utf-8"));
|
|
219
|
+
// Runtime shape validation — reject corrupted or tampered state
|
|
220
|
+
if (typeof raw !== "object" ||
|
|
221
|
+
raw === null ||
|
|
222
|
+
typeof raw.planFilename !== "string" ||
|
|
223
|
+
typeof raw.startedAt !== "string" ||
|
|
224
|
+
typeof raw.maxRetries !== "number" ||
|
|
225
|
+
typeof raw.currentTaskIndex !== "number" ||
|
|
226
|
+
!Array.isArray(raw.tasks)) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
return raw;
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Write REPL state to .cortex/repl-state.json.
|
|
237
|
+
* Uses atomic write (temp file + rename) to prevent corruption.
|
|
238
|
+
*/
|
|
239
|
+
export function writeReplState(cwd, state) {
|
|
240
|
+
const filepath = statePath(cwd);
|
|
241
|
+
const dir = path.dirname(filepath);
|
|
242
|
+
if (!fs.existsSync(dir)) {
|
|
243
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
244
|
+
}
|
|
245
|
+
// Atomic write: write to unique temp file, then rename
|
|
246
|
+
const suffix = crypto.randomBytes(6).toString("hex");
|
|
247
|
+
const tmpPath = `${filepath}.${suffix}.tmp`;
|
|
248
|
+
try {
|
|
249
|
+
fs.writeFileSync(tmpPath, JSON.stringify(state, null, 2));
|
|
250
|
+
fs.renameSync(tmpPath, filepath);
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
// Clean up temp file on failure
|
|
254
|
+
try {
|
|
255
|
+
fs.unlinkSync(tmpPath);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// Ignore cleanup errors
|
|
259
|
+
}
|
|
260
|
+
throw err;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Get the next pending task (first task with status "pending").
|
|
265
|
+
* Returns null if all tasks are done.
|
|
266
|
+
*/
|
|
267
|
+
export function getNextTask(state) {
|
|
268
|
+
return state.tasks.find((t) => t.status === "pending") ?? null;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Get the currently in-progress task.
|
|
272
|
+
* Returns null if no task is in progress.
|
|
273
|
+
*/
|
|
274
|
+
export function getCurrentTask(state) {
|
|
275
|
+
return state.tasks.find((t) => t.status === "in_progress") ?? null;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Check if the loop is complete (no pending or in_progress tasks).
|
|
279
|
+
*/
|
|
280
|
+
export function isLoopComplete(state) {
|
|
281
|
+
return state.tasks.every((t) => t.status === "passed" || t.status === "failed" || t.status === "skipped");
|
|
282
|
+
}
|
|
283
|
+
// ─── Formatting ──────────────────────────────────────────────────────────────
|
|
284
|
+
/** Visual progress bar using block characters. */
|
|
285
|
+
function progressBar(done, total, width = 20) {
|
|
286
|
+
if (total === 0)
|
|
287
|
+
return "░".repeat(width);
|
|
288
|
+
const filled = Math.round((done / total) * width);
|
|
289
|
+
return "█".repeat(filled) + "░".repeat(width - filled);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Format the current loop status as a human-readable string.
|
|
293
|
+
* Used by repl_status tool output.
|
|
294
|
+
*/
|
|
295
|
+
export function formatProgress(state) {
|
|
296
|
+
const total = state.tasks.length;
|
|
297
|
+
const passed = state.tasks.filter((t) => t.status === "passed").length;
|
|
298
|
+
const failed = state.tasks.filter((t) => t.status === "failed").length;
|
|
299
|
+
const skipped = state.tasks.filter((t) => t.status === "skipped").length;
|
|
300
|
+
const done = passed + failed + skipped;
|
|
301
|
+
const pending = state.tasks.filter((t) => t.status === "pending").length;
|
|
302
|
+
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
303
|
+
const lines = [];
|
|
304
|
+
lines.push(`Progress: ${progressBar(done, total)} ${done}/${total} tasks (${pct}%)`);
|
|
305
|
+
lines.push(` Passed: ${passed} | Failed: ${failed} | Skipped: ${skipped} | Pending: ${pending}`);
|
|
306
|
+
// Current or next task
|
|
307
|
+
const current = getCurrentTask(state);
|
|
308
|
+
const next = getNextTask(state);
|
|
309
|
+
if (current) {
|
|
310
|
+
lines.push("");
|
|
311
|
+
lines.push(`Current Task (#${current.index + 1}):`);
|
|
312
|
+
lines.push(` "${current.description}"`);
|
|
313
|
+
if (current.retries > 0) {
|
|
314
|
+
lines.push(` Attempt: ${current.retries + 1}/${state.maxRetries}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
else if (next) {
|
|
318
|
+
lines.push("");
|
|
319
|
+
lines.push(`Next Task (#${next.index + 1}):`);
|
|
320
|
+
lines.push(` "${next.description}"`);
|
|
321
|
+
}
|
|
322
|
+
else if (isLoopComplete(state)) {
|
|
323
|
+
lines.push("");
|
|
324
|
+
lines.push("All tasks complete.");
|
|
325
|
+
}
|
|
326
|
+
// Commands
|
|
327
|
+
lines.push("");
|
|
328
|
+
lines.push(`Build: ${state.buildCommand || "(not detected)"}`);
|
|
329
|
+
lines.push(`Test: ${state.testCommand || "(not detected)"}`);
|
|
330
|
+
if (state.lintCommand) {
|
|
331
|
+
lines.push(`Lint: ${state.lintCommand}`);
|
|
332
|
+
}
|
|
333
|
+
lines.push(`Max retries: ${state.maxRetries}`);
|
|
334
|
+
// Task history
|
|
335
|
+
lines.push("");
|
|
336
|
+
lines.push("Task History:");
|
|
337
|
+
for (const task of state.tasks) {
|
|
338
|
+
const num = `#${task.index + 1}`;
|
|
339
|
+
const iterInfo = task.iterations.length > 0
|
|
340
|
+
? ` (${task.iterations.length} iteration${task.iterations.length > 1 ? "s" : ""}${task.retries > 0 ? `, ${task.retries} retr${task.retries > 1 ? "ies" : "y"}` : ""})`
|
|
341
|
+
: "";
|
|
342
|
+
switch (task.status) {
|
|
343
|
+
case "passed":
|
|
344
|
+
lines.push(` \u2713 ${num} ${task.description}${iterInfo}`);
|
|
345
|
+
break;
|
|
346
|
+
case "failed":
|
|
347
|
+
lines.push(` \u2717 ${num} ${task.description}${iterInfo}`);
|
|
348
|
+
break;
|
|
349
|
+
case "skipped":
|
|
350
|
+
lines.push(` \u2298 ${num} ${task.description}`);
|
|
351
|
+
break;
|
|
352
|
+
case "in_progress":
|
|
353
|
+
lines.push(` \u25B6 ${num} ${task.description}${iterInfo}`);
|
|
354
|
+
break;
|
|
355
|
+
case "pending":
|
|
356
|
+
lines.push(` \u25CB ${num} ${task.description}`);
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return lines.join("\n");
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Format a full summary of the loop results for PR body inclusion.
|
|
364
|
+
* Returns a markdown block with a results table, counts, and timing.
|
|
365
|
+
*/
|
|
366
|
+
export function formatSummary(state) {
|
|
367
|
+
const total = state.tasks.length;
|
|
368
|
+
const passed = state.tasks.filter((t) => t.status === "passed").length;
|
|
369
|
+
const failed = state.tasks.filter((t) => t.status === "failed").length;
|
|
370
|
+
const skipped = state.tasks.filter((t) => t.status === "skipped").length;
|
|
371
|
+
const totalIterations = state.tasks.reduce((sum, t) => sum + t.iterations.length, 0);
|
|
372
|
+
const lines = [];
|
|
373
|
+
lines.push("## REPL Loop Summary");
|
|
374
|
+
lines.push("");
|
|
375
|
+
lines.push("| # | Task | Status | Attempts |");
|
|
376
|
+
lines.push("|---|------|--------|----------|");
|
|
377
|
+
for (const task of state.tasks) {
|
|
378
|
+
const num = task.index + 1;
|
|
379
|
+
// Truncate long descriptions for the table and sanitize for markdown
|
|
380
|
+
const rawDesc = task.description.length > 60
|
|
381
|
+
? task.description.substring(0, 57) + "..."
|
|
382
|
+
: task.description;
|
|
383
|
+
const desc = rawDesc.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
384
|
+
let statusIcon;
|
|
385
|
+
let attempts;
|
|
386
|
+
switch (task.status) {
|
|
387
|
+
case "passed":
|
|
388
|
+
statusIcon = "Passed";
|
|
389
|
+
attempts = String(task.iterations.length);
|
|
390
|
+
break;
|
|
391
|
+
case "failed":
|
|
392
|
+
statusIcon = "Failed";
|
|
393
|
+
attempts = String(task.iterations.length);
|
|
394
|
+
break;
|
|
395
|
+
case "skipped":
|
|
396
|
+
statusIcon = "Skipped";
|
|
397
|
+
attempts = "—";
|
|
398
|
+
break;
|
|
399
|
+
case "in_progress":
|
|
400
|
+
statusIcon = "In Progress";
|
|
401
|
+
attempts = String(task.iterations.length);
|
|
402
|
+
break;
|
|
403
|
+
default:
|
|
404
|
+
statusIcon = "Pending";
|
|
405
|
+
attempts = "—";
|
|
406
|
+
}
|
|
407
|
+
lines.push(`| ${num} | ${desc} | ${statusIcon} | ${attempts} |`);
|
|
408
|
+
}
|
|
409
|
+
lines.push("");
|
|
410
|
+
lines.push(`**Results: ${passed} passed, ${failed} failed, ${skipped} skipped** (${totalIterations} total iterations)`);
|
|
411
|
+
// Timing
|
|
412
|
+
if (state.startedAt) {
|
|
413
|
+
const start = new Date(state.startedAt);
|
|
414
|
+
const end = state.completedAt ? new Date(state.completedAt) : new Date();
|
|
415
|
+
const durationMs = end.getTime() - start.getTime();
|
|
416
|
+
const durationMin = Math.round(durationMs / 60_000);
|
|
417
|
+
lines.push(`Duration: ${durationMin > 0 ? `${durationMin} minute${durationMin > 1 ? "s" : ""}` : "< 1 minute"}`);
|
|
418
|
+
}
|
|
419
|
+
lines.push(`Plan: ${state.planFilename}`);
|
|
420
|
+
// List failed tasks with details if any
|
|
421
|
+
const failedTasks = state.tasks.filter((t) => t.status === "failed");
|
|
422
|
+
if (failedTasks.length > 0) {
|
|
423
|
+
lines.push("");
|
|
424
|
+
lines.push("### Failed Tasks");
|
|
425
|
+
for (const task of failedTasks) {
|
|
426
|
+
lines.push(`- **#${task.index + 1}**: ${task.description}`);
|
|
427
|
+
const lastIter = task.iterations[task.iterations.length - 1];
|
|
428
|
+
if (lastIter) {
|
|
429
|
+
lines.push(` Last error: ${lastIter.detail.substring(0, 200)}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return lines.join("\n");
|
|
434
|
+
}
|
package/dist/utils/terminal.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface TerminalSession {
|
|
|
21
21
|
dbusPath?: string;
|
|
22
22
|
pid?: number;
|
|
23
23
|
ptyId?: string;
|
|
24
|
-
mode: "terminal" | "pty" | "background";
|
|
24
|
+
mode: "terminal" | "pty" | "background" | "ide";
|
|
25
25
|
branch: string;
|
|
26
26
|
agent: string;
|
|
27
27
|
worktreePath: string;
|
|
@@ -63,4 +63,56 @@ export declare function getDriverByName(name: string): TerminalDriver | null;
|
|
|
63
63
|
* This is the main entry point for worktree_remove cleanup.
|
|
64
64
|
*/
|
|
65
65
|
export declare function closeSession(session: TerminalSession): Promise<boolean>;
|
|
66
|
+
/**
|
|
67
|
+
* Check if a given driver is an IDE driver (VS Code, Cursor, Windsurf, Zed, JetBrains).
|
|
68
|
+
* Used to determine whether to attempt a fallback when the IDE CLI is unavailable.
|
|
69
|
+
*/
|
|
70
|
+
export declare function isIDEDriver(driver: TerminalDriver): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Detect the first non-IDE terminal driver that matches the environment.
|
|
73
|
+
* Used as a fallback when the IDE CLI is not available (e.g., `code` not in PATH).
|
|
74
|
+
*
|
|
75
|
+
* Skips IDE drivers and JetBrains, tries tmux, iTerm2, Terminal.app, kitty, etc.
|
|
76
|
+
* Returns null if no terminal driver matches (only fallback driver left).
|
|
77
|
+
*/
|
|
78
|
+
export declare function detectFallbackDriver(): TerminalDriver | null;
|
|
79
|
+
/**
|
|
80
|
+
* Check if we're currently inside an IDE's integrated terminal.
|
|
81
|
+
* Returns the IDE driver if detected, null otherwise.
|
|
82
|
+
*/
|
|
83
|
+
export declare function detectIDE(): TerminalDriver | null;
|
|
84
|
+
/**
|
|
85
|
+
* Check if a specific IDE's CLI is available on the system.
|
|
86
|
+
*/
|
|
87
|
+
export declare function isIDECliAvailable(ideName: string): Promise<boolean>;
|
|
88
|
+
/**
|
|
89
|
+
* Get a list of all available IDE CLIs on the system.
|
|
90
|
+
* Useful for offering launch options.
|
|
91
|
+
*/
|
|
92
|
+
export declare function getAvailableIDEs(): Promise<string[]>;
|
|
93
|
+
/**
|
|
94
|
+
* Result from the multi-strategy detection chain.
|
|
95
|
+
* Includes the matched driver and how it was found (for diagnostics).
|
|
96
|
+
*/
|
|
97
|
+
export interface TerminalDetectionResult {
|
|
98
|
+
driver: TerminalDriver;
|
|
99
|
+
strategy: "env" | "process-tree" | "frontmost-app" | "user-config" | "fallback";
|
|
100
|
+
detail?: string;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Async multi-strategy detection of the user's terminal emulator.
|
|
104
|
+
*
|
|
105
|
+
* This is the PRIMARY function for "Open in terminal tab" — it NEVER returns
|
|
106
|
+
* an IDE driver. Use `detectDriver()` for IDE-first detection (e.g., "Open in IDE").
|
|
107
|
+
*
|
|
108
|
+
* Strategy chain:
|
|
109
|
+
* 1. Environment variables (fast, synchronous)
|
|
110
|
+
* 2. Process-tree walk (macOS: ps, Linux: /proc)
|
|
111
|
+
* 3. Frontmost application (macOS: AppleScript)
|
|
112
|
+
* 4. User preference (.cortex/config.json)
|
|
113
|
+
* 5. FallbackDriver (last resort)
|
|
114
|
+
*
|
|
115
|
+
* @param projectRoot — optional project root for reading .cortex/config.json
|
|
116
|
+
*/
|
|
117
|
+
export declare function detectTerminalDriver(projectRoot?: string): Promise<TerminalDetectionResult>;
|
|
66
118
|
//# sourceMappingURL=terminal.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"terminal.d.ts","sourceRoot":"","sources":["../../src/utils/terminal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH,yDAAyD;AACzD,MAAM,WAAW,eAAe;IAC9B,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;IAG1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IAGf,IAAI,EAAE,UAAU,GAAG,KAAK,GAAG,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"terminal.d.ts","sourceRoot":"","sources":["../../src/utils/terminal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH,yDAAyD;AACzD,MAAM,WAAW,eAAe;IAC9B,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;IAG1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IAGf,IAAI,EAAE,UAAU,GAAG,KAAK,GAAG,YAAY,GAAG,KAAK,CAAC;IAChD,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qDAAqD;AACrD,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,iDAAiD;AACjD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,MAAM,IAAI,OAAO,CAAC;IAClB,OAAO,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC;IACjE,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtD;AAMD,yEAAyE;AACzE,wBAAgB,YAAY,CAC1B,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,eAAe,GACvB,IAAI,CASN;AAED,wEAAwE;AACxE,wBAAgB,WAAW,CACzB,YAAY,EAAE,MAAM,GACnB,eAAe,GAAG,IAAI,CAQxB;AA68BD;;;GAGG;AACH,wBAAgB,YAAY,IAAI,cAAc,CAM7C;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAEnE;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAY7E;AAID;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAE3D;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,IAAI,cAAc,GAAG,IAAI,CAU5D;AAED;;;GAGG;AACH,wBAAgB,SAAS,IAAI,cAAc,GAAG,IAAI,CAajD;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAazE;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAW1D;AAMD;;;GAGG;AACH,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE,KAAK,GAAG,cAAc,GAAG,eAAe,GAAG,aAAa,GAAG,UAAU,CAAC;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA8MD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,oBAAoB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC,CA2BjG"}
|