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.
Files changed (50) hide show
  1. package/.opencode/agents/{plan.md → architect.md} +69 -15
  2. package/.opencode/agents/{fullstack.md → crosslayer.md} +2 -2
  3. package/.opencode/agents/{debug.md → fix.md} +11 -9
  4. package/.opencode/agents/{security.md → guard.md} +1 -1
  5. package/.opencode/agents/{build.md → implement.md} +121 -22
  6. package/.opencode/agents/{testing.md → qa.md} +1 -1
  7. package/.opencode/agents/{devops.md → ship.md} +1 -1
  8. package/README.md +107 -31
  9. package/dist/cli.js +87 -16
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +47 -7
  12. package/dist/registry.d.ts +8 -3
  13. package/dist/registry.d.ts.map +1 -1
  14. package/dist/registry.js +16 -2
  15. package/dist/tools/cortex.d.ts +2 -2
  16. package/dist/tools/cortex.js +7 -7
  17. package/dist/tools/environment.d.ts +31 -0
  18. package/dist/tools/environment.d.ts.map +1 -0
  19. package/dist/tools/environment.js +93 -0
  20. package/dist/tools/github.d.ts +42 -0
  21. package/dist/tools/github.d.ts.map +1 -0
  22. package/dist/tools/github.js +200 -0
  23. package/dist/tools/repl.d.ts +50 -0
  24. package/dist/tools/repl.d.ts.map +1 -0
  25. package/dist/tools/repl.js +240 -0
  26. package/dist/tools/task.d.ts +2 -0
  27. package/dist/tools/task.d.ts.map +1 -1
  28. package/dist/tools/task.js +25 -30
  29. package/dist/tools/worktree.d.ts.map +1 -1
  30. package/dist/tools/worktree.js +22 -11
  31. package/dist/utils/github.d.ts +104 -0
  32. package/dist/utils/github.d.ts.map +1 -0
  33. package/dist/utils/github.js +243 -0
  34. package/dist/utils/ide.d.ts +76 -0
  35. package/dist/utils/ide.d.ts.map +1 -0
  36. package/dist/utils/ide.js +307 -0
  37. package/dist/utils/plan-extract.d.ts +7 -0
  38. package/dist/utils/plan-extract.d.ts.map +1 -1
  39. package/dist/utils/plan-extract.js +25 -1
  40. package/dist/utils/repl.d.ts +114 -0
  41. package/dist/utils/repl.d.ts.map +1 -0
  42. package/dist/utils/repl.js +434 -0
  43. package/dist/utils/terminal.d.ts +53 -1
  44. package/dist/utils/terminal.d.ts.map +1 -1
  45. package/dist/utils/terminal.js +642 -5
  46. package/package.json +1 -1
  47. package/dist/plugin.d.ts +0 -1
  48. package/dist/plugin.d.ts.map +0 -1
  49. package/dist/plugin.js +0 -4
  50. /package/.opencode/agents/{review.md → audit.md} +0 -0
@@ -2,6 +2,25 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  const CORTEX_DIR = ".cortex";
4
4
  const PLANS_DIR = "plans";
5
+ /**
6
+ * Extract GitHub issue references from plan frontmatter.
7
+ *
8
+ * Looks for `issues: [42, 51]` in YAML frontmatter.
9
+ * Returns an array of issue numbers, or an empty array if none found.
10
+ */
11
+ export function extractIssueRefs(planContent) {
12
+ const frontmatterMatch = planContent.match(/^---\n([\s\S]*?)\n---/);
13
+ if (!frontmatterMatch)
14
+ return [];
15
+ // Match issues: [42, 51] or issues: [42]
16
+ const issuesMatch = frontmatterMatch[1].match(/issues:\s*\[([^\]]*)\]/);
17
+ if (!issuesMatch)
18
+ return [];
19
+ return issuesMatch[1]
20
+ .split(",")
21
+ .map((s) => parseInt(s.trim(), 10))
22
+ .filter((n) => !isNaN(n) && n > 0);
23
+ }
5
24
  /**
6
25
  * Extract relevant sections from a plan markdown file for composing a PR body.
7
26
  *
@@ -106,7 +125,12 @@ export function findPlanContent(worktree, planFilename, branchName) {
106
125
  if (!fs.existsSync(plansDir))
107
126
  return null;
108
127
  if (planFilename) {
109
- const filepath = path.join(plansDir, planFilename);
128
+ // Prevent path traversal — resolve and verify the path is strictly inside plansDir
129
+ const filepath = path.resolve(plansDir, planFilename);
130
+ const resolvedPlansDir = path.resolve(plansDir);
131
+ if (!filepath.startsWith(resolvedPlansDir + path.sep)) {
132
+ return null; // Reject traversal attempts and directory-level references (".", "")
133
+ }
110
134
  if (fs.existsSync(filepath)) {
111
135
  return { content: fs.readFileSync(filepath, "utf-8"), filename: planFilename };
112
136
  }
@@ -0,0 +1,114 @@
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
+ export type TaskStatus = "pending" | "in_progress" | "passed" | "failed" | "skipped";
11
+ export interface TaskIteration {
12
+ /** ISO timestamp of this iteration */
13
+ at: string;
14
+ /** Outcome of this iteration */
15
+ result: "pass" | "fail" | "skip";
16
+ /** Test output summary, error message, or skip reason */
17
+ detail: string;
18
+ }
19
+ export interface ReplTask {
20
+ /** Zero-based index in the task list */
21
+ index: number;
22
+ /** Task description from the plan */
23
+ description: string;
24
+ /** Current status in the state machine */
25
+ status: TaskStatus;
26
+ /** Number of failed attempts (resets on pass) */
27
+ retries: number;
28
+ /** Full iteration history */
29
+ iterations: TaskIteration[];
30
+ }
31
+ export interface ReplState {
32
+ /** Source plan filename */
33
+ planFilename: string;
34
+ /** ISO timestamp when the loop started */
35
+ startedAt: string;
36
+ /** ISO timestamp when the loop completed (all tasks done or aborted) */
37
+ completedAt?: string;
38
+ /** Auto-detected or user-provided build command */
39
+ buildCommand: string | null;
40
+ /** Auto-detected or user-provided test command */
41
+ testCommand: string | null;
42
+ /** Optional lint command */
43
+ lintCommand: string | null;
44
+ /** Max retries per task before escalating to user (default: 3) */
45
+ maxRetries: number;
46
+ /** Index of the currently active task (-1 if not started) */
47
+ currentTaskIndex: number;
48
+ /** All tasks in the loop */
49
+ tasks: ReplTask[];
50
+ }
51
+ export interface CommandDetection {
52
+ buildCommand: string | null;
53
+ testCommand: string | null;
54
+ lintCommand: string | null;
55
+ /** Detected framework name (e.g., "vitest", "jest", "pytest") */
56
+ framework: string;
57
+ /** Whether auto-detection found anything */
58
+ detected: boolean;
59
+ }
60
+ /**
61
+ * Parse plan tasks from plan markdown content.
62
+ *
63
+ * Looks for unchecked checkbox items (`- [ ] ...`) in a `## Tasks` section.
64
+ * Falls back to any unchecked checkboxes anywhere in the document.
65
+ * Strips the `Task N:` prefix if present to get a clean description.
66
+ */
67
+ export declare function parseTasksFromPlan(planContent: string): string[];
68
+ /**
69
+ * Auto-detect build, test, and lint commands from project configuration files.
70
+ *
71
+ * Detection priority:
72
+ * 1. package.json (npm/node projects)
73
+ * 2. Makefile
74
+ * 3. Cargo.toml (Rust)
75
+ * 4. go.mod (Go)
76
+ * 5. pyproject.toml / setup.py (Python)
77
+ * 6. mix.exs (Elixir)
78
+ */
79
+ export declare function detectCommands(cwd: string): Promise<CommandDetection>;
80
+ /**
81
+ * Read the current REPL state from .cortex/repl-state.json.
82
+ * Returns null if no state file exists.
83
+ */
84
+ export declare function readReplState(cwd: string): ReplState | null;
85
+ /**
86
+ * Write REPL state to .cortex/repl-state.json.
87
+ * Uses atomic write (temp file + rename) to prevent corruption.
88
+ */
89
+ export declare function writeReplState(cwd: string, state: ReplState): void;
90
+ /**
91
+ * Get the next pending task (first task with status "pending").
92
+ * Returns null if all tasks are done.
93
+ */
94
+ export declare function getNextTask(state: ReplState): ReplTask | null;
95
+ /**
96
+ * Get the currently in-progress task.
97
+ * Returns null if no task is in progress.
98
+ */
99
+ export declare function getCurrentTask(state: ReplState): ReplTask | null;
100
+ /**
101
+ * Check if the loop is complete (no pending or in_progress tasks).
102
+ */
103
+ export declare function isLoopComplete(state: ReplState): boolean;
104
+ /**
105
+ * Format the current loop status as a human-readable string.
106
+ * Used by repl_status tool output.
107
+ */
108
+ export declare function formatProgress(state: ReplState): string;
109
+ /**
110
+ * Format a full summary of the loop results for PR body inclusion.
111
+ * Returns a markdown block with a results table, counts, and timing.
112
+ */
113
+ export declare function formatSummary(state: ReplState): string;
114
+ //# sourceMappingURL=repl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repl.d.ts","sourceRoot":"","sources":["../../src/utils/repl.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAaH,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;AAErF,MAAM,WAAW,aAAa;IAC5B,sCAAsC;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,gCAAgC;IAChC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IACjC,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,QAAQ;IACvB,wCAAwC;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,qCAAqC;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,0CAA0C;IAC1C,MAAM,EAAE,UAAU,CAAC;IACnB,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,6BAA6B;IAC7B,UAAU,EAAE,aAAa,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,SAAS;IACxB,2BAA2B;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mDAAmD;IACnD,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,kDAAkD;IAClD,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,4BAA4B;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,kEAAkE;IAClE,UAAU,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,gBAAgB,EAAE,MAAM,CAAC;IACzB,4BAA4B;IAC5B,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,iEAAiE;IACjE,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,QAAQ,EAAE,OAAO,CAAC;CACnB;AAID;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAsBhE;AA4BD;;;;;;;;;;GAUG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA6H3E;AAWD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAwB3D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,IAAI,CAuBlE;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,GAAG,QAAQ,GAAG,IAAI,CAE7D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,GAAG,QAAQ,GAAG,IAAI,CAEhE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAIxD;AAWD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAwEvD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CA+EtD"}
@@ -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
+ }
@@ -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"}