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.
Files changed (70) hide show
  1. package/.opencode/agents/{plan.md → architect.md} +104 -58
  2. package/.opencode/agents/audit.md +183 -0
  3. package/.opencode/agents/{fullstack.md → coder.md} +10 -54
  4. package/.opencode/agents/debug.md +76 -201
  5. package/.opencode/agents/devops.md +16 -123
  6. package/.opencode/agents/docs-writer.md +195 -0
  7. package/.opencode/agents/fix.md +207 -0
  8. package/.opencode/agents/implement.md +433 -0
  9. package/.opencode/agents/perf.md +151 -0
  10. package/.opencode/agents/refactor.md +163 -0
  11. package/.opencode/agents/security.md +20 -85
  12. package/.opencode/agents/testing.md +1 -151
  13. package/.opencode/skills/data-engineering/SKILL.md +221 -0
  14. package/.opencode/skills/monitoring-observability/SKILL.md +251 -0
  15. package/README.md +315 -224
  16. package/dist/cli.js +85 -17
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +60 -22
  19. package/dist/registry.d.ts +8 -3
  20. package/dist/registry.d.ts.map +1 -1
  21. package/dist/registry.js +16 -2
  22. package/dist/tools/branch.d.ts +2 -2
  23. package/dist/tools/cortex.d.ts +2 -2
  24. package/dist/tools/cortex.js +7 -7
  25. package/dist/tools/docs.d.ts +2 -2
  26. package/dist/tools/environment.d.ts +31 -0
  27. package/dist/tools/environment.d.ts.map +1 -0
  28. package/dist/tools/environment.js +93 -0
  29. package/dist/tools/github.d.ts +42 -0
  30. package/dist/tools/github.d.ts.map +1 -0
  31. package/dist/tools/github.js +200 -0
  32. package/dist/tools/plan.d.ts +28 -4
  33. package/dist/tools/plan.d.ts.map +1 -1
  34. package/dist/tools/plan.js +232 -4
  35. package/dist/tools/quality-gate.d.ts +28 -0
  36. package/dist/tools/quality-gate.d.ts.map +1 -0
  37. package/dist/tools/quality-gate.js +233 -0
  38. package/dist/tools/repl.d.ts +55 -0
  39. package/dist/tools/repl.d.ts.map +1 -0
  40. package/dist/tools/repl.js +291 -0
  41. package/dist/tools/task.d.ts +2 -0
  42. package/dist/tools/task.d.ts.map +1 -1
  43. package/dist/tools/task.js +25 -30
  44. package/dist/tools/worktree.d.ts +5 -32
  45. package/dist/tools/worktree.d.ts.map +1 -1
  46. package/dist/tools/worktree.js +75 -447
  47. package/dist/utils/change-scope.d.ts +33 -0
  48. package/dist/utils/change-scope.d.ts.map +1 -0
  49. package/dist/utils/change-scope.js +198 -0
  50. package/dist/utils/github.d.ts +104 -0
  51. package/dist/utils/github.d.ts.map +1 -0
  52. package/dist/utils/github.js +243 -0
  53. package/dist/utils/ide.d.ts +76 -0
  54. package/dist/utils/ide.d.ts.map +1 -0
  55. package/dist/utils/ide.js +307 -0
  56. package/dist/utils/plan-extract.d.ts +28 -0
  57. package/dist/utils/plan-extract.d.ts.map +1 -1
  58. package/dist/utils/plan-extract.js +90 -1
  59. package/dist/utils/repl.d.ts +145 -0
  60. package/dist/utils/repl.d.ts.map +1 -0
  61. package/dist/utils/repl.js +547 -0
  62. package/dist/utils/terminal.d.ts +53 -1
  63. package/dist/utils/terminal.d.ts.map +1 -1
  64. package/dist/utils/terminal.js +642 -5
  65. package/package.json +1 -1
  66. package/.opencode/agents/build.md +0 -294
  67. package/.opencode/agents/review.md +0 -314
  68. package/dist/plugin.d.ts +0 -1
  69. package/dist/plugin.d.ts.map +0 -1
  70. 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
+ }
@@ -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;IACxC,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;AA6kBD;;;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"}
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"}