cortex-agents 2.3.1 → 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.
- package/.opencode/agents/{plan.md → architect.md} +104 -58
- package/.opencode/agents/audit.md +183 -0
- package/.opencode/agents/{fullstack.md → coder.md} +10 -54
- package/.opencode/agents/debug.md +76 -201
- package/.opencode/agents/devops.md +16 -123
- package/.opencode/agents/docs-writer.md +195 -0
- package/.opencode/agents/fix.md +207 -0
- package/.opencode/agents/implement.md +433 -0
- package/.opencode/agents/perf.md +151 -0
- package/.opencode/agents/refactor.md +163 -0
- package/.opencode/agents/security.md +20 -85
- package/.opencode/agents/testing.md +1 -151
- package/.opencode/skills/data-engineering/SKILL.md +221 -0
- package/.opencode/skills/monitoring-observability/SKILL.md +251 -0
- package/README.md +315 -224
- package/dist/cli.js +85 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +60 -22
- package/dist/registry.d.ts +8 -3
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +16 -2
- package/dist/tools/branch.d.ts +2 -2
- package/dist/tools/cortex.d.ts +2 -2
- package/dist/tools/cortex.js +7 -7
- package/dist/tools/docs.d.ts +2 -2
- 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/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 +55 -0
- package/dist/tools/repl.d.ts.map +1 -0
- package/dist/tools/repl.js +291 -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 +5 -32
- package/dist/tools/worktree.d.ts.map +1 -1
- package/dist/tools/worktree.js +75 -447
- 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/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 +28 -0
- package/dist/utils/plan-extract.d.ts.map +1 -1
- package/dist/utils/plan-extract.js +90 -1
- package/dist/utils/repl.d.ts +145 -0
- package/dist/utils/repl.d.ts.map +1 -0
- package/dist/utils/repl.js +547 -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/build.md +0 -294
- package/.opencode/agents/review.md +0 -314
- package/dist/plugin.d.ts +0 -1
- package/dist/plugin.d.ts.map +0 -1
- package/dist/plugin.js +0 -4
|
@@ -0,0 +1,547 @@
|
|
|
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
|
+
/**
|
|
17
|
+
* Parse plan tasks from plan markdown content.
|
|
18
|
+
*
|
|
19
|
+
* Looks for unchecked checkbox items (`- [ ] ...`) in a `## Tasks` section.
|
|
20
|
+
* Falls back to any unchecked checkboxes anywhere in the document.
|
|
21
|
+
* Strips the `Task N:` prefix if present to get a clean description.
|
|
22
|
+
* Extracts `- AC:` lines immediately following each task as acceptance criteria.
|
|
23
|
+
*/
|
|
24
|
+
export function parseTasksFromPlan(planContent) {
|
|
25
|
+
return parseTasksWithAC(planContent).map((t) => t.description);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse plan tasks with their acceptance criteria.
|
|
29
|
+
*
|
|
30
|
+
* Returns structured tasks including `- AC:` lines found under each checkbox item.
|
|
31
|
+
*/
|
|
32
|
+
export function parseTasksWithAC(planContent) {
|
|
33
|
+
const tasksSection = extractTasksSection(planContent);
|
|
34
|
+
const source = tasksSection || planContent;
|
|
35
|
+
const tasks = [];
|
|
36
|
+
const lines = source.split("\n");
|
|
37
|
+
for (let i = 0; i < lines.length; i++) {
|
|
38
|
+
const match = lines[i].match(/^[-*]\s*\[\s\]\s+(.+)$/);
|
|
39
|
+
if (match) {
|
|
40
|
+
let description = match[1].trim();
|
|
41
|
+
description = description.replace(/^Task\s+\d+\s*:\s*/i, "");
|
|
42
|
+
if (!description)
|
|
43
|
+
continue;
|
|
44
|
+
// Collect AC lines immediately following this task
|
|
45
|
+
const acs = [];
|
|
46
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
47
|
+
const acMatch = lines[j].match(/^\s+[-*]\s*AC:\s*(.+)$/);
|
|
48
|
+
if (acMatch) {
|
|
49
|
+
acs.push(acMatch[1].trim());
|
|
50
|
+
}
|
|
51
|
+
else if (lines[j].match(/^[-*]\s*\[/)) {
|
|
52
|
+
// Next task checkbox — stop collecting ACs
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
else if (lines[j].trim() === "") {
|
|
56
|
+
// Blank line — continue (may be spacing between AC lines)
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// Non-AC, non-blank, non-checkbox line — stop
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
tasks.push({ description, acceptanceCriteria: acs });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return tasks;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Extract the content of the ## Tasks section from plan markdown.
|
|
71
|
+
* Returns null if no Tasks section found.
|
|
72
|
+
*/
|
|
73
|
+
function extractTasksSection(content) {
|
|
74
|
+
const lines = content.split("\n");
|
|
75
|
+
let inTasksSection = false;
|
|
76
|
+
const sectionLines = [];
|
|
77
|
+
for (const line of lines) {
|
|
78
|
+
if (/^##\s+Tasks/i.test(line)) {
|
|
79
|
+
inTasksSection = true;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (inTasksSection) {
|
|
83
|
+
// End of section when we hit another ## heading
|
|
84
|
+
if (/^##\s+/.test(line))
|
|
85
|
+
break;
|
|
86
|
+
sectionLines.push(line);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return sectionLines.length > 0 ? sectionLines.join("\n") : null;
|
|
90
|
+
}
|
|
91
|
+
// ─── Command Auto-Detection ──────────────────────────────────────────────────
|
|
92
|
+
/**
|
|
93
|
+
* Auto-detect build, test, and lint commands from project configuration files.
|
|
94
|
+
*
|
|
95
|
+
* Detection priority:
|
|
96
|
+
* 1. package.json (npm/node projects)
|
|
97
|
+
* 2. Makefile
|
|
98
|
+
* 3. Cargo.toml (Rust)
|
|
99
|
+
* 4. go.mod (Go)
|
|
100
|
+
* 5. pyproject.toml / setup.py (Python)
|
|
101
|
+
* 6. mix.exs (Elixir)
|
|
102
|
+
*/
|
|
103
|
+
export async function detectCommands(cwd) {
|
|
104
|
+
const result = {
|
|
105
|
+
buildCommand: null,
|
|
106
|
+
testCommand: null,
|
|
107
|
+
lintCommand: null,
|
|
108
|
+
framework: "unknown",
|
|
109
|
+
detected: false,
|
|
110
|
+
};
|
|
111
|
+
// 1. Check package.json (most common for this project type)
|
|
112
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
113
|
+
if (fs.existsSync(pkgPath)) {
|
|
114
|
+
try {
|
|
115
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
116
|
+
const scripts = pkg.scripts || {};
|
|
117
|
+
const devDeps = pkg.devDependencies || {};
|
|
118
|
+
const deps = pkg.dependencies || {};
|
|
119
|
+
// Build command
|
|
120
|
+
if (scripts.build) {
|
|
121
|
+
result.buildCommand = "npm run build";
|
|
122
|
+
}
|
|
123
|
+
// Test command — prefer specific runner detection
|
|
124
|
+
if (devDeps.vitest || deps.vitest) {
|
|
125
|
+
result.testCommand = "npx vitest run";
|
|
126
|
+
result.framework = "vitest";
|
|
127
|
+
}
|
|
128
|
+
else if (devDeps.jest || deps.jest) {
|
|
129
|
+
result.testCommand = "npx jest";
|
|
130
|
+
result.framework = "jest";
|
|
131
|
+
}
|
|
132
|
+
else if (devDeps.mocha || deps.mocha) {
|
|
133
|
+
result.testCommand = "npx mocha";
|
|
134
|
+
result.framework = "mocha";
|
|
135
|
+
}
|
|
136
|
+
else if (scripts.test && scripts.test !== 'echo "Error: no test specified" && exit 1') {
|
|
137
|
+
result.testCommand = "npm test";
|
|
138
|
+
result.framework = "npm-test";
|
|
139
|
+
}
|
|
140
|
+
// Lint command
|
|
141
|
+
if (scripts.lint) {
|
|
142
|
+
result.lintCommand = "npm run lint";
|
|
143
|
+
}
|
|
144
|
+
result.detected = !!(result.buildCommand || result.testCommand);
|
|
145
|
+
if (result.detected)
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// Malformed package.json — continue to next detector
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// 2. Check Makefile
|
|
153
|
+
const makefilePath = path.join(cwd, "Makefile");
|
|
154
|
+
if (fs.existsSync(makefilePath)) {
|
|
155
|
+
try {
|
|
156
|
+
const makefile = fs.readFileSync(makefilePath, "utf-8");
|
|
157
|
+
if (/^build\s*:/m.test(makefile)) {
|
|
158
|
+
result.buildCommand = "make build";
|
|
159
|
+
}
|
|
160
|
+
if (/^test\s*:/m.test(makefile)) {
|
|
161
|
+
result.testCommand = "make test";
|
|
162
|
+
result.framework = "make";
|
|
163
|
+
}
|
|
164
|
+
if (/^lint\s*:/m.test(makefile)) {
|
|
165
|
+
result.lintCommand = "make lint";
|
|
166
|
+
}
|
|
167
|
+
result.detected = !!(result.buildCommand || result.testCommand);
|
|
168
|
+
if (result.detected)
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// Continue to next detector
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// 3. Check Cargo.toml (Rust)
|
|
176
|
+
if (fs.existsSync(path.join(cwd, "Cargo.toml"))) {
|
|
177
|
+
result.buildCommand = "cargo build";
|
|
178
|
+
result.testCommand = "cargo test";
|
|
179
|
+
result.framework = "cargo";
|
|
180
|
+
result.detected = true;
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
// 4. Check go.mod (Go)
|
|
184
|
+
if (fs.existsSync(path.join(cwd, "go.mod"))) {
|
|
185
|
+
result.buildCommand = "go build ./...";
|
|
186
|
+
result.testCommand = "go test ./...";
|
|
187
|
+
result.framework = "go-test";
|
|
188
|
+
result.detected = true;
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
// 5. Check pyproject.toml or setup.py (Python)
|
|
192
|
+
const pyprojectPath = path.join(cwd, "pyproject.toml");
|
|
193
|
+
if (fs.existsSync(pyprojectPath)) {
|
|
194
|
+
try {
|
|
195
|
+
const pyproject = fs.readFileSync(pyprojectPath, "utf-8");
|
|
196
|
+
if (pyproject.includes("pytest")) {
|
|
197
|
+
result.testCommand = "pytest";
|
|
198
|
+
result.framework = "pytest";
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
result.testCommand = "python -m pytest";
|
|
202
|
+
result.framework = "pytest";
|
|
203
|
+
}
|
|
204
|
+
result.detected = true;
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// Continue
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (fs.existsSync(path.join(cwd, "setup.py"))) {
|
|
212
|
+
result.testCommand = "python -m pytest";
|
|
213
|
+
result.framework = "pytest";
|
|
214
|
+
result.detected = true;
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
// 6. Check mix.exs (Elixir)
|
|
218
|
+
if (fs.existsSync(path.join(cwd, "mix.exs"))) {
|
|
219
|
+
result.buildCommand = "mix compile";
|
|
220
|
+
result.testCommand = "mix test";
|
|
221
|
+
result.framework = "ExUnit";
|
|
222
|
+
result.detected = true;
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
// ─── Config Reading ─────────────────────────────────────────────────────────
|
|
228
|
+
const CONFIG_FILE = "config.json";
|
|
229
|
+
/**
|
|
230
|
+
* Read cortex config from .cortex/config.json.
|
|
231
|
+
* Returns an empty config if the file doesn't exist or is malformed.
|
|
232
|
+
*/
|
|
233
|
+
export function readCortexConfig(cwd) {
|
|
234
|
+
const configPath = path.join(cwd, CORTEX_DIR, CONFIG_FILE);
|
|
235
|
+
if (!fs.existsSync(configPath))
|
|
236
|
+
return {};
|
|
237
|
+
try {
|
|
238
|
+
const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
239
|
+
if (typeof raw !== "object" || raw === null)
|
|
240
|
+
return {};
|
|
241
|
+
return {
|
|
242
|
+
maxRetries: typeof raw.maxRetries === "number" ? raw.maxRetries : undefined,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return {};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// ─── State Management ────────────────────────────────────────────────────────
|
|
250
|
+
/**
|
|
251
|
+
* Get the path to the REPL state file.
|
|
252
|
+
*/
|
|
253
|
+
function statePath(cwd) {
|
|
254
|
+
return path.join(cwd, CORTEX_DIR, REPL_STATE_FILE);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Read the current REPL state from .cortex/repl-state.json.
|
|
258
|
+
* Returns null if no state file exists.
|
|
259
|
+
*/
|
|
260
|
+
export function readReplState(cwd) {
|
|
261
|
+
const filepath = statePath(cwd);
|
|
262
|
+
if (!fs.existsSync(filepath))
|
|
263
|
+
return null;
|
|
264
|
+
try {
|
|
265
|
+
const raw = JSON.parse(fs.readFileSync(filepath, "utf-8"));
|
|
266
|
+
// Runtime shape validation — reject corrupted or tampered state
|
|
267
|
+
if (typeof raw !== "object" ||
|
|
268
|
+
raw === null ||
|
|
269
|
+
typeof raw.planFilename !== "string" ||
|
|
270
|
+
typeof raw.startedAt !== "string" ||
|
|
271
|
+
typeof raw.maxRetries !== "number" ||
|
|
272
|
+
typeof raw.currentTaskIndex !== "number" ||
|
|
273
|
+
!Array.isArray(raw.tasks)) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
// Backward compatibility: ensure all tasks have acceptanceCriteria
|
|
277
|
+
for (const task of raw.tasks) {
|
|
278
|
+
if (!Array.isArray(task.acceptanceCriteria)) {
|
|
279
|
+
task.acceptanceCriteria = [];
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return raw;
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Write REPL state to .cortex/repl-state.json.
|
|
290
|
+
* Uses atomic write (temp file + rename) to prevent corruption.
|
|
291
|
+
*/
|
|
292
|
+
export function writeReplState(cwd, state) {
|
|
293
|
+
const filepath = statePath(cwd);
|
|
294
|
+
const dir = path.dirname(filepath);
|
|
295
|
+
if (!fs.existsSync(dir)) {
|
|
296
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
297
|
+
}
|
|
298
|
+
// Atomic write: write to unique temp file, then rename
|
|
299
|
+
const suffix = crypto.randomBytes(6).toString("hex");
|
|
300
|
+
const tmpPath = `${filepath}.${suffix}.tmp`;
|
|
301
|
+
try {
|
|
302
|
+
fs.writeFileSync(tmpPath, JSON.stringify(state, null, 2));
|
|
303
|
+
fs.renameSync(tmpPath, filepath);
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
// Clean up temp file on failure
|
|
307
|
+
try {
|
|
308
|
+
fs.unlinkSync(tmpPath);
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
// Ignore cleanup errors
|
|
312
|
+
}
|
|
313
|
+
throw err;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Get the next pending task (first task with status "pending").
|
|
318
|
+
* Returns null if all tasks are done.
|
|
319
|
+
*/
|
|
320
|
+
export function getNextTask(state) {
|
|
321
|
+
return state.tasks.find((t) => t.status === "pending") ?? null;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Get the currently in-progress task.
|
|
325
|
+
* Returns null if no task is in progress.
|
|
326
|
+
*/
|
|
327
|
+
export function getCurrentTask(state) {
|
|
328
|
+
return state.tasks.find((t) => t.status === "in_progress") ?? null;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Check if the loop is complete (no pending or in_progress tasks).
|
|
332
|
+
*/
|
|
333
|
+
export function isLoopComplete(state) {
|
|
334
|
+
return state.tasks.every((t) => t.status === "passed" || t.status === "failed" || t.status === "skipped");
|
|
335
|
+
}
|
|
336
|
+
// ─── Formatting ──────────────────────────────────────────────────────────────
|
|
337
|
+
/**
|
|
338
|
+
* Detect if a previous REPL loop was interrupted mid-task.
|
|
339
|
+
* Returns the incomplete state if found, null otherwise.
|
|
340
|
+
*/
|
|
341
|
+
export function detectIncompleteState(cwd) {
|
|
342
|
+
const state = readReplState(cwd);
|
|
343
|
+
if (!state)
|
|
344
|
+
return null;
|
|
345
|
+
// Loop is already complete
|
|
346
|
+
if (isLoopComplete(state))
|
|
347
|
+
return null;
|
|
348
|
+
// There's an in_progress task — session was interrupted
|
|
349
|
+
const current = getCurrentTask(state);
|
|
350
|
+
if (current)
|
|
351
|
+
return state;
|
|
352
|
+
// There are pending tasks but no in_progress — also incomplete
|
|
353
|
+
const next = getNextTask(state);
|
|
354
|
+
if (next)
|
|
355
|
+
return state;
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Format task duration as a human-readable string.
|
|
360
|
+
*/
|
|
361
|
+
function formatTaskDuration(task) {
|
|
362
|
+
if (!task.startedAt)
|
|
363
|
+
return null;
|
|
364
|
+
const start = new Date(task.startedAt);
|
|
365
|
+
const end = task.completedAt ? new Date(task.completedAt) : new Date();
|
|
366
|
+
const durationMs = end.getTime() - start.getTime();
|
|
367
|
+
if (durationMs < 1000)
|
|
368
|
+
return "< 1s";
|
|
369
|
+
if (durationMs < 60_000)
|
|
370
|
+
return `${Math.round(durationMs / 1000)}s`;
|
|
371
|
+
const mins = Math.floor(durationMs / 60_000);
|
|
372
|
+
const secs = Math.round((durationMs % 60_000) / 1000);
|
|
373
|
+
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
374
|
+
}
|
|
375
|
+
/** Visual progress bar using block characters. */
|
|
376
|
+
function progressBar(done, total, width = 20) {
|
|
377
|
+
if (total === 0)
|
|
378
|
+
return "░".repeat(width);
|
|
379
|
+
const filled = Math.round((done / total) * width);
|
|
380
|
+
return "█".repeat(filled) + "░".repeat(width - filled);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Format the current loop status as a human-readable string.
|
|
384
|
+
* Used by repl_status tool output.
|
|
385
|
+
*/
|
|
386
|
+
export function formatProgress(state) {
|
|
387
|
+
const total = state.tasks.length;
|
|
388
|
+
const passed = state.tasks.filter((t) => t.status === "passed").length;
|
|
389
|
+
const failed = state.tasks.filter((t) => t.status === "failed").length;
|
|
390
|
+
const skipped = state.tasks.filter((t) => t.status === "skipped").length;
|
|
391
|
+
const done = passed + failed + skipped;
|
|
392
|
+
const pending = state.tasks.filter((t) => t.status === "pending").length;
|
|
393
|
+
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
394
|
+
const lines = [];
|
|
395
|
+
lines.push(`Progress: ${progressBar(done, total)} ${done}/${total} tasks (${pct}%)`);
|
|
396
|
+
lines.push(` Passed: ${passed} | Failed: ${failed} | Skipped: ${skipped} | Pending: ${pending}`);
|
|
397
|
+
// Current or next task
|
|
398
|
+
const current = getCurrentTask(state);
|
|
399
|
+
const next = getNextTask(state);
|
|
400
|
+
if (current) {
|
|
401
|
+
lines.push("");
|
|
402
|
+
lines.push(`Current Task (#${current.index + 1}):`);
|
|
403
|
+
lines.push(` "${current.description}"`);
|
|
404
|
+
if (current.retries > 0) {
|
|
405
|
+
lines.push(` Attempt: ${current.retries + 1}/${state.maxRetries}`);
|
|
406
|
+
}
|
|
407
|
+
if (current.acceptanceCriteria.length > 0) {
|
|
408
|
+
lines.push(` Acceptance Criteria:`);
|
|
409
|
+
for (const ac of current.acceptanceCriteria) {
|
|
410
|
+
lines.push(` - ${ac}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
else if (next) {
|
|
415
|
+
lines.push("");
|
|
416
|
+
lines.push(`Next Task (#${next.index + 1}):`);
|
|
417
|
+
lines.push(` "${next.description}"`);
|
|
418
|
+
if (next.acceptanceCriteria.length > 0) {
|
|
419
|
+
lines.push(` Acceptance Criteria:`);
|
|
420
|
+
for (const ac of next.acceptanceCriteria) {
|
|
421
|
+
lines.push(` - ${ac}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
else if (isLoopComplete(state)) {
|
|
426
|
+
lines.push("");
|
|
427
|
+
lines.push("All tasks complete.");
|
|
428
|
+
}
|
|
429
|
+
// Commands
|
|
430
|
+
lines.push("");
|
|
431
|
+
lines.push(`Build: ${state.buildCommand || "(not detected)"}`);
|
|
432
|
+
lines.push(`Test: ${state.testCommand || "(not detected)"}`);
|
|
433
|
+
if (state.lintCommand) {
|
|
434
|
+
lines.push(`Lint: ${state.lintCommand}`);
|
|
435
|
+
}
|
|
436
|
+
lines.push(`Max retries: ${state.maxRetries}`);
|
|
437
|
+
// Task history
|
|
438
|
+
lines.push("");
|
|
439
|
+
lines.push("Task History:");
|
|
440
|
+
for (const task of state.tasks) {
|
|
441
|
+
const num = `#${task.index + 1}`;
|
|
442
|
+
const iterInfo = task.iterations.length > 0
|
|
443
|
+
? ` (${task.iterations.length} iteration${task.iterations.length > 1 ? "s" : ""}${task.retries > 0 ? `, ${task.retries} retr${task.retries > 1 ? "ies" : "y"}` : ""})`
|
|
444
|
+
: "";
|
|
445
|
+
const timeInfo = formatTaskDuration(task);
|
|
446
|
+
const timeSuffix = timeInfo ? ` [${timeInfo}]` : "";
|
|
447
|
+
switch (task.status) {
|
|
448
|
+
case "passed":
|
|
449
|
+
lines.push(` \u2713 ${num} ${task.description}${iterInfo}${timeSuffix}`);
|
|
450
|
+
break;
|
|
451
|
+
case "failed":
|
|
452
|
+
lines.push(` \u2717 ${num} ${task.description}${iterInfo}${timeSuffix}`);
|
|
453
|
+
break;
|
|
454
|
+
case "skipped":
|
|
455
|
+
lines.push(` \u2298 ${num} ${task.description}${timeSuffix}`);
|
|
456
|
+
break;
|
|
457
|
+
case "in_progress":
|
|
458
|
+
lines.push(` \u25B6 ${num} ${task.description}${iterInfo}${timeSuffix}`);
|
|
459
|
+
break;
|
|
460
|
+
case "pending":
|
|
461
|
+
lines.push(` \u25CB ${num} ${task.description}`);
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return lines.join("\n");
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Format a full summary of the loop results for PR body inclusion.
|
|
469
|
+
* Returns a markdown block with a results table, counts, and timing.
|
|
470
|
+
*/
|
|
471
|
+
export function formatSummary(state) {
|
|
472
|
+
const total = state.tasks.length;
|
|
473
|
+
const passed = state.tasks.filter((t) => t.status === "passed").length;
|
|
474
|
+
const failed = state.tasks.filter((t) => t.status === "failed").length;
|
|
475
|
+
const skipped = state.tasks.filter((t) => t.status === "skipped").length;
|
|
476
|
+
const totalIterations = state.tasks.reduce((sum, t) => sum + t.iterations.length, 0);
|
|
477
|
+
const lines = [];
|
|
478
|
+
lines.push("## REPL Loop Summary");
|
|
479
|
+
lines.push("");
|
|
480
|
+
lines.push("| # | Task | Status | Attempts |");
|
|
481
|
+
lines.push("|---|------|--------|----------|");
|
|
482
|
+
for (const task of state.tasks) {
|
|
483
|
+
const num = task.index + 1;
|
|
484
|
+
// Truncate long descriptions for the table and sanitize for markdown
|
|
485
|
+
const rawDesc = task.description.length > 60
|
|
486
|
+
? task.description.substring(0, 57) + "..."
|
|
487
|
+
: task.description;
|
|
488
|
+
const desc = rawDesc.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
489
|
+
let statusIcon;
|
|
490
|
+
let attempts;
|
|
491
|
+
switch (task.status) {
|
|
492
|
+
case "passed":
|
|
493
|
+
statusIcon = "Passed";
|
|
494
|
+
attempts = String(task.iterations.length);
|
|
495
|
+
break;
|
|
496
|
+
case "failed":
|
|
497
|
+
statusIcon = "Failed";
|
|
498
|
+
attempts = String(task.iterations.length);
|
|
499
|
+
break;
|
|
500
|
+
case "skipped":
|
|
501
|
+
statusIcon = "Skipped";
|
|
502
|
+
attempts = "—";
|
|
503
|
+
break;
|
|
504
|
+
case "in_progress":
|
|
505
|
+
statusIcon = "In Progress";
|
|
506
|
+
attempts = String(task.iterations.length);
|
|
507
|
+
break;
|
|
508
|
+
default:
|
|
509
|
+
statusIcon = "Pending";
|
|
510
|
+
attempts = "—";
|
|
511
|
+
}
|
|
512
|
+
lines.push(`| ${num} | ${desc} | ${statusIcon} | ${attempts} |`);
|
|
513
|
+
}
|
|
514
|
+
lines.push("");
|
|
515
|
+
const totalACs = state.tasks.reduce((sum, t) => sum + t.acceptanceCriteria.length, 0);
|
|
516
|
+
const passedACs = state.tasks
|
|
517
|
+
.filter((t) => t.status === "passed")
|
|
518
|
+
.reduce((sum, t) => sum + t.acceptanceCriteria.length, 0);
|
|
519
|
+
let resultsLine = `**Results: ${passed} passed, ${failed} failed, ${skipped} skipped** (${totalIterations} total iterations)`;
|
|
520
|
+
if (totalACs > 0) {
|
|
521
|
+
resultsLine += ` | **ACs: ${passedACs}/${totalACs} satisfied**`;
|
|
522
|
+
}
|
|
523
|
+
lines.push(resultsLine);
|
|
524
|
+
// Timing
|
|
525
|
+
if (state.startedAt) {
|
|
526
|
+
const start = new Date(state.startedAt);
|
|
527
|
+
const end = state.completedAt ? new Date(state.completedAt) : new Date();
|
|
528
|
+
const durationMs = end.getTime() - start.getTime();
|
|
529
|
+
const durationMin = Math.round(durationMs / 60_000);
|
|
530
|
+
lines.push(`Duration: ${durationMin > 0 ? `${durationMin} minute${durationMin > 1 ? "s" : ""}` : "< 1 minute"}`);
|
|
531
|
+
}
|
|
532
|
+
lines.push(`Plan: ${state.planFilename}`);
|
|
533
|
+
// List failed tasks with details if any
|
|
534
|
+
const failedTasks = state.tasks.filter((t) => t.status === "failed");
|
|
535
|
+
if (failedTasks.length > 0) {
|
|
536
|
+
lines.push("");
|
|
537
|
+
lines.push("### Failed Tasks");
|
|
538
|
+
for (const task of failedTasks) {
|
|
539
|
+
lines.push(`- **#${task.index + 1}**: ${task.description}`);
|
|
540
|
+
const lastIter = task.iterations[task.iterations.length - 1];
|
|
541
|
+
if (lastIter) {
|
|
542
|
+
lines.push(` Last error: ${lastIter.detail.substring(0, 200)}`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return lines.join("\n");
|
|
547
|
+
}
|
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"}
|