cortex-agents 2.3.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.opencode/agents/{plan.md → architect.md} +104 -45
  2. package/.opencode/agents/audit.md +314 -0
  3. package/.opencode/agents/crosslayer.md +218 -0
  4. package/.opencode/agents/{debug.md → fix.md} +75 -46
  5. package/.opencode/agents/guard.md +202 -0
  6. package/.opencode/agents/{build.md → implement.md} +151 -107
  7. package/.opencode/agents/qa.md +265 -0
  8. package/.opencode/agents/ship.md +249 -0
  9. package/README.md +119 -31
  10. package/dist/cli.js +87 -16
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +215 -9
  13. package/dist/registry.d.ts +8 -3
  14. package/dist/registry.d.ts.map +1 -1
  15. package/dist/registry.js +16 -2
  16. package/dist/tools/cortex.d.ts +2 -2
  17. package/dist/tools/cortex.js +7 -7
  18. package/dist/tools/environment.d.ts +31 -0
  19. package/dist/tools/environment.d.ts.map +1 -0
  20. package/dist/tools/environment.js +93 -0
  21. package/dist/tools/github.d.ts +42 -0
  22. package/dist/tools/github.d.ts.map +1 -0
  23. package/dist/tools/github.js +200 -0
  24. package/dist/tools/repl.d.ts +50 -0
  25. package/dist/tools/repl.d.ts.map +1 -0
  26. package/dist/tools/repl.js +240 -0
  27. package/dist/tools/task.d.ts +2 -0
  28. package/dist/tools/task.d.ts.map +1 -1
  29. package/dist/tools/task.js +25 -30
  30. package/dist/tools/worktree.d.ts.map +1 -1
  31. package/dist/tools/worktree.js +22 -11
  32. package/dist/utils/github.d.ts +104 -0
  33. package/dist/utils/github.d.ts.map +1 -0
  34. package/dist/utils/github.js +243 -0
  35. package/dist/utils/ide.d.ts +76 -0
  36. package/dist/utils/ide.d.ts.map +1 -0
  37. package/dist/utils/ide.js +307 -0
  38. package/dist/utils/plan-extract.d.ts +7 -0
  39. package/dist/utils/plan-extract.d.ts.map +1 -1
  40. package/dist/utils/plan-extract.js +25 -1
  41. package/dist/utils/repl.d.ts +114 -0
  42. package/dist/utils/repl.d.ts.map +1 -0
  43. package/dist/utils/repl.js +434 -0
  44. package/dist/utils/terminal.d.ts +53 -1
  45. package/dist/utils/terminal.d.ts.map +1 -1
  46. package/dist/utils/terminal.js +642 -5
  47. package/package.json +1 -1
  48. package/.opencode/agents/devops.md +0 -176
  49. package/.opencode/agents/fullstack.md +0 -171
  50. package/.opencode/agents/security.md +0 -148
  51. package/.opencode/agents/testing.md +0 -132
  52. package/dist/plugin.d.ts +0 -1
  53. package/dist/plugin.d.ts.map +0 -1
  54. package/dist/plugin.js +0 -4
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Environment Detection Tool
3
+ *
4
+ * Provides agents with information about the current development environment
5
+ * (IDE, terminal, editor) to offer contextually appropriate worktree launch options.
6
+ *
7
+ * Uses two detection systems:
8
+ * - IDE detection (ide.ts) — for "Open in IDE" options
9
+ * - Terminal detection (terminal.ts) — for "Open in terminal tab" options
10
+ *
11
+ * The terminal detection uses a multi-strategy chain:
12
+ * 1. Environment variables (fast, synchronous)
13
+ * 2. Process-tree walk (finds terminal in parent processes)
14
+ * 3. Frontmost app detection (macOS only)
15
+ * 4. User preference (.cortex/config.json)
16
+ */
17
+ import { tool } from "@opencode-ai/plugin";
18
+ import { detectIDEWithCLICheck, formatEnvironmentReport, generateEnvironmentRecommendations } from "../utils/ide.js";
19
+ import { detectDriver, detectTerminalDriver } from "../utils/terminal.js";
20
+ export const detectEnvironment = tool({
21
+ description: "Detect the current development environment (IDE, terminal, editor) " +
22
+ "to offer contextually appropriate worktree launch options. " +
23
+ "Returns environment info and recommended launch options.",
24
+ args: {},
25
+ async execute(args, context) {
26
+ // Use async detection that verifies CLI availability in PATH
27
+ const ide = await detectIDEWithCLICheck();
28
+ const ideDriver = detectDriver();
29
+ // Multi-strategy terminal detection (the one used for "Open in terminal tab")
30
+ const terminalDetection = await detectTerminalDriver(context.worktree);
31
+ // Format the report (now includes CLI availability status)
32
+ const report = formatEnvironmentReport(ide, terminalDetection.driver.name);
33
+ // Add CLI status section
34
+ const additionalInfo = [];
35
+ additionalInfo.push(``, `### CLI Status`, ``);
36
+ additionalInfo.push(`- IDE: ${ide.name}`);
37
+ additionalInfo.push(`- IDE Driver: ${ideDriver.name}`);
38
+ additionalInfo.push(`- Terminal Emulator: **${terminalDetection.driver.name}**`);
39
+ additionalInfo.push(`- Platform: ${process.platform}`);
40
+ if (ide.cliBinary) {
41
+ if (ide.cliAvailable) {
42
+ additionalInfo.push(`- CLI: \`${ide.cliBinary}\` available in PATH`);
43
+ }
44
+ else {
45
+ additionalInfo.push(`- CLI: \`${ide.cliBinary}\` **NOT found** in PATH`);
46
+ if (ide.cliInstallHint) {
47
+ additionalInfo.push(`- Fix: ${ide.cliInstallHint}`);
48
+ }
49
+ }
50
+ }
51
+ // Terminal detection details
52
+ additionalInfo.push(``, `### Terminal Detection`, ``);
53
+ additionalInfo.push(`- Strategy: **${terminalDetection.strategy}**`);
54
+ if (terminalDetection.detail) {
55
+ additionalInfo.push(`- Detail: ${terminalDetection.detail}`);
56
+ }
57
+ additionalInfo.push(`- Driver: ${terminalDetection.driver.name}`);
58
+ additionalInfo.push(``);
59
+ additionalInfo.push(`When "Open in terminal tab" is selected, a new tab will open in **${terminalDetection.driver.name}**.`);
60
+ return report + additionalInfo.join("\n");
61
+ },
62
+ });
63
+ /**
64
+ * Quick environment check tool for agents.
65
+ * Returns a simplified response for quick decision-making.
66
+ */
67
+ export const getEnvironmentInfo = tool({
68
+ description: "Get quick environment info for deciding worktree launch options. " +
69
+ "Returns IDE type, terminal name, and whether to offer IDE-specific options.",
70
+ args: {},
71
+ async execute(args, context) {
72
+ const ide = await detectIDEWithCLICheck();
73
+ const terminalDetection = await detectTerminalDriver(context.worktree);
74
+ return JSON.stringify({
75
+ ide: {
76
+ type: ide.type,
77
+ name: ide.name,
78
+ hasIntegratedTerminal: ide.hasIntegratedTerminal,
79
+ canOpenInWindow: ide.canOpenInWindow,
80
+ cliAvailable: ide.cliAvailable,
81
+ cliBinary: ide.cliBinary,
82
+ cliInstallHint: ide.cliInstallHint,
83
+ },
84
+ terminal: {
85
+ name: terminalDetection.driver.name,
86
+ detectionStrategy: terminalDetection.strategy,
87
+ detectionDetail: terminalDetection.detail,
88
+ },
89
+ platform: process.platform,
90
+ recommendations: generateEnvironmentRecommendations(ide),
91
+ }, null, 2);
92
+ },
93
+ });
@@ -0,0 +1,42 @@
1
+ export declare const status: {
2
+ description: string;
3
+ args: {};
4
+ execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
5
+ };
6
+ export declare const issues: {
7
+ description: string;
8
+ args: {
9
+ state: import("zod").ZodOptional<import("zod").ZodEnum<{
10
+ closed: "closed";
11
+ open: "open";
12
+ all: "all";
13
+ }>>;
14
+ labels: import("zod").ZodOptional<import("zod").ZodString>;
15
+ milestone: import("zod").ZodOptional<import("zod").ZodString>;
16
+ assignee: import("zod").ZodOptional<import("zod").ZodString>;
17
+ limit: import("zod").ZodOptional<import("zod").ZodNumber>;
18
+ detailed: import("zod").ZodOptional<import("zod").ZodBoolean>;
19
+ };
20
+ execute(args: {
21
+ state?: "closed" | "open" | "all" | undefined;
22
+ labels?: string | undefined;
23
+ milestone?: string | undefined;
24
+ assignee?: string | undefined;
25
+ limit?: number | undefined;
26
+ detailed?: boolean | undefined;
27
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
28
+ };
29
+ export declare const projects: {
30
+ description: string;
31
+ args: {
32
+ projectNumber: import("zod").ZodOptional<import("zod").ZodNumber>;
33
+ status: import("zod").ZodOptional<import("zod").ZodString>;
34
+ limit: import("zod").ZodOptional<import("zod").ZodNumber>;
35
+ };
36
+ execute(args: {
37
+ projectNumber?: number | undefined;
38
+ status?: string | undefined;
39
+ limit?: number | undefined;
40
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
41
+ };
42
+ //# sourceMappingURL=github.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../../src/tools/github.ts"],"names":[],"mappings":"AAwCA,eAAO,MAAM,MAAM;;;;CAgDjB,CAAC;AAIH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;CAsEjB,CAAC;AAIH,eAAO,MAAM,QAAQ;;;;;;;;;;;;CA0EnB,CAAC"}
@@ -0,0 +1,200 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { checkGhAvailability, fetchIssues, fetchProjects, fetchProjectItems, formatIssueList, formatIssueForPlan, formatProjectItemList, } from "../utils/github.js";
3
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
4
+ /**
5
+ * Validate GitHub CLI availability and return status.
6
+ * Returns a formatted error string if not available, or the status object.
7
+ */
8
+ async function requireGh(cwd) {
9
+ const status = await checkGhAvailability(cwd);
10
+ if (!status.installed) {
11
+ return {
12
+ ok: false,
13
+ error: "✗ GitHub CLI (gh) is not installed.\n\nInstall it from https://cli.github.com/ and run `gh auth login`.",
14
+ };
15
+ }
16
+ if (!status.authenticated) {
17
+ return {
18
+ ok: false,
19
+ error: "✗ GitHub CLI is not authenticated.\n\nRun `gh auth login` to authenticate.",
20
+ };
21
+ }
22
+ return { ok: true, status };
23
+ }
24
+ // ─── Tool: github_status ─────────────────────────────────────────────────────
25
+ export const status = tool({
26
+ description: "Check GitHub CLI availability, authentication, and detect GitHub Projects for the current repository. " +
27
+ "Use this first to verify the repo is connected before listing issues or projects.",
28
+ args: {},
29
+ async execute(_args, context) {
30
+ const status = await checkGhAvailability(context.worktree);
31
+ const lines = [];
32
+ // Installation
33
+ lines.push(status.installed ? "✓ GitHub CLI installed" : "✗ GitHub CLI not installed");
34
+ if (!status.installed) {
35
+ lines.push(" Install from https://cli.github.com/");
36
+ return lines.join("\n");
37
+ }
38
+ // Authentication
39
+ lines.push(status.authenticated ? "✓ Authenticated" : "✗ Not authenticated");
40
+ if (!status.authenticated) {
41
+ lines.push(" Run: gh auth login");
42
+ return lines.join("\n");
43
+ }
44
+ // Remote
45
+ if (status.hasRemote && status.repoOwner && status.repoName) {
46
+ lines.push(`✓ Repository: ${status.repoOwner}/${status.repoName}`);
47
+ // Fetch projects
48
+ const projects = await fetchProjects(context.worktree, status.repoOwner);
49
+ status.projects = projects;
50
+ if (projects.length > 0) {
51
+ lines.push("");
52
+ lines.push(`GitHub Projects (${projects.length}):`);
53
+ for (const p of projects) {
54
+ lines.push(` #${p.number}: ${p.title}`);
55
+ }
56
+ }
57
+ else {
58
+ lines.push(" No GitHub Projects found for this repository owner.");
59
+ }
60
+ }
61
+ else {
62
+ lines.push("✗ No GitHub remote (origin) configured");
63
+ lines.push(" Add one with: git remote add origin <url>");
64
+ }
65
+ return lines.join("\n");
66
+ },
67
+ });
68
+ // ─── Tool: github_issues ─────────────────────────────────────────────────────
69
+ export const issues = tool({
70
+ description: "List GitHub issues for the current repository, filterable by state, labels, milestone, and assignee. " +
71
+ "Returns a formatted list suitable for selecting issues to plan. " +
72
+ "Set `detailed` to true to get full issue descriptions for plan seeding.",
73
+ args: {
74
+ state: tool.schema
75
+ .enum(["open", "closed", "all"])
76
+ .optional()
77
+ .describe("Filter by issue state (default: open)"),
78
+ labels: tool.schema
79
+ .string()
80
+ .optional()
81
+ .describe("Filter by labels (comma-separated, e.g., 'bug,priority:high')"),
82
+ milestone: tool.schema
83
+ .string()
84
+ .optional()
85
+ .describe("Filter by milestone name"),
86
+ assignee: tool.schema
87
+ .string()
88
+ .optional()
89
+ .describe("Filter by assignee username"),
90
+ limit: tool.schema
91
+ .number()
92
+ .optional()
93
+ .describe("Maximum number of issues to return (default: 20, max: 100)"),
94
+ detailed: tool.schema
95
+ .boolean()
96
+ .optional()
97
+ .describe("If true, return full issue details formatted for plan seeding (default: false)"),
98
+ },
99
+ async execute(args, context) {
100
+ const check = await requireGh(context.worktree);
101
+ if (!check.ok)
102
+ return check.error;
103
+ const limit = Math.min(Math.max(args.limit ?? 20, 1), 100);
104
+ try {
105
+ const issueList = await fetchIssues(context.worktree, {
106
+ state: args.state ?? "open",
107
+ labels: args.labels,
108
+ milestone: args.milestone,
109
+ assignee: args.assignee,
110
+ limit,
111
+ });
112
+ if (issueList.length === 0) {
113
+ const filters = [];
114
+ if (args.state)
115
+ filters.push(`state: ${args.state}`);
116
+ if (args.labels)
117
+ filters.push(`labels: ${args.labels}`);
118
+ if (args.milestone)
119
+ filters.push(`milestone: ${args.milestone}`);
120
+ if (args.assignee)
121
+ filters.push(`assignee: ${args.assignee}`);
122
+ const filterStr = filters.length > 0 ? ` (filters: ${filters.join(", ")})` : "";
123
+ return `No issues found${filterStr}.`;
124
+ }
125
+ const header = `Found ${issueList.length} issue(s):\n\n`;
126
+ if (args.detailed) {
127
+ // Full detail mode for plan seeding
128
+ const formatted = issueList.map((issue) => formatIssueForPlan(issue)).join("\n\n---\n\n");
129
+ return header + formatted;
130
+ }
131
+ // Compact list mode for selection
132
+ return header + formatIssueList(issueList);
133
+ }
134
+ catch (error) {
135
+ return `✗ Error fetching issues: ${error.message || error}`;
136
+ }
137
+ },
138
+ });
139
+ // ─── Tool: github_projects ───────────────────────────────────────────────────
140
+ export const projects = tool({
141
+ description: "List GitHub Project boards and their work items for the current repository. " +
142
+ "Without a projectNumber, lists all projects. " +
143
+ "With a projectNumber, lists items from that specific project board.",
144
+ args: {
145
+ projectNumber: tool.schema
146
+ .number()
147
+ .optional()
148
+ .describe("Specific project number to list items from (omit to list all projects)"),
149
+ status: tool.schema
150
+ .string()
151
+ .optional()
152
+ .describe("Filter project items by status column (e.g., 'Todo', 'In Progress')"),
153
+ limit: tool.schema
154
+ .number()
155
+ .optional()
156
+ .describe("Maximum number of items to return (default: 30)"),
157
+ },
158
+ async execute(args, context) {
159
+ const check = await requireGh(context.worktree);
160
+ if (!check.ok)
161
+ return check.error;
162
+ const { status: ghStatus } = check;
163
+ if (!ghStatus.hasRemote || !ghStatus.repoOwner) {
164
+ return "✗ No GitHub remote (origin) configured. Cannot fetch projects.";
165
+ }
166
+ const owner = ghStatus.repoOwner;
167
+ try {
168
+ // If no project number specified, list all projects
169
+ if (args.projectNumber === undefined) {
170
+ const projectList = await fetchProjects(context.worktree, owner);
171
+ if (projectList.length === 0) {
172
+ return `No GitHub Projects found for ${owner}.`;
173
+ }
174
+ const lines = [`GitHub Projects for ${owner} (${projectList.length}):\n`];
175
+ for (const p of projectList) {
176
+ lines.push(` #${p.number}: ${p.title}`);
177
+ }
178
+ lines.push("");
179
+ lines.push("Use github_projects with a projectNumber to list items from a specific project.");
180
+ return lines.join("\n");
181
+ }
182
+ // Fetch items from a specific project (cap limit for safety)
183
+ const limit = Math.min(Math.max(args.limit ?? 30, 1), 100);
184
+ const items = await fetchProjectItems(context.worktree, owner, args.projectNumber, {
185
+ status: args.status,
186
+ limit,
187
+ });
188
+ if (items.length === 0) {
189
+ const statusFilter = args.status ? ` with status "${args.status}"` : "";
190
+ return `No items found in project #${args.projectNumber}${statusFilter}.`;
191
+ }
192
+ const statusFilter = args.status ? ` (status: ${args.status})` : "";
193
+ const header = `Project #${args.projectNumber} — ${items.length} item(s)${statusFilter}:\n\n`;
194
+ return header + formatProjectItemList(items);
195
+ }
196
+ catch (error) {
197
+ return `✗ Error fetching projects: ${error.message || error}`;
198
+ }
199
+ },
200
+ });
@@ -0,0 +1,50 @@
1
+ /**
2
+ * REPL Loop Tools
3
+ *
4
+ * Four tools for the implement agent's iterative task-by-task development loop:
5
+ * - repl_init — Initialize a loop from a plan
6
+ * - repl_status — Get current progress and next task
7
+ * - repl_report — Report task outcome (pass/fail/skip)
8
+ * - repl_summary — Generate markdown summary for PR body
9
+ */
10
+ export declare const init: {
11
+ description: string;
12
+ args: {
13
+ planFilename: import("zod").ZodString;
14
+ buildCommand: import("zod").ZodOptional<import("zod").ZodString>;
15
+ testCommand: import("zod").ZodOptional<import("zod").ZodString>;
16
+ maxRetries: import("zod").ZodOptional<import("zod").ZodNumber>;
17
+ };
18
+ execute(args: {
19
+ planFilename: string;
20
+ buildCommand?: string | undefined;
21
+ testCommand?: string | undefined;
22
+ maxRetries?: number | undefined;
23
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
24
+ };
25
+ export declare const status: {
26
+ description: string;
27
+ args: {};
28
+ execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
29
+ };
30
+ export declare const report: {
31
+ description: string;
32
+ args: {
33
+ result: import("zod").ZodEnum<{
34
+ pass: "pass";
35
+ fail: "fail";
36
+ skip: "skip";
37
+ }>;
38
+ detail: import("zod").ZodString;
39
+ };
40
+ execute(args: {
41
+ result: "pass" | "fail" | "skip";
42
+ detail: string;
43
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
44
+ };
45
+ export declare const summary: {
46
+ description: string;
47
+ args: {};
48
+ execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
49
+ };
50
+ //# sourceMappingURL=repl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repl.d.ts","sourceRoot":"","sources":["../../src/tools/repl.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAwBH,eAAO,MAAM,IAAI;;;;;;;;;;;;;;CAqGf,CAAC;AAIH,eAAO,MAAM,MAAM;;;;CAyBjB,CAAC;AAIH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;CAwCjB,CAAC;AA8EH,eAAO,MAAM,OAAO;;;;CAclB,CAAC"}
@@ -0,0 +1,240 @@
1
+ /**
2
+ * REPL Loop Tools
3
+ *
4
+ * Four tools for the implement agent's iterative task-by-task development loop:
5
+ * - repl_init — Initialize a loop from a plan
6
+ * - repl_status — Get current progress and next task
7
+ * - repl_report — Report task outcome (pass/fail/skip)
8
+ * - repl_summary — Generate markdown summary for PR body
9
+ */
10
+ import { tool } from "@opencode-ai/plugin";
11
+ import * as fs from "fs";
12
+ import * as path from "path";
13
+ import { parseTasksFromPlan, detectCommands, readReplState, writeReplState, getNextTask, getCurrentTask, isLoopComplete, formatProgress, formatSummary, } from "../utils/repl.js";
14
+ const CORTEX_DIR = ".cortex";
15
+ const PLANS_DIR = "plans";
16
+ // ─── repl_init ───────────────────────────────────────────────────────────────
17
+ export const init = tool({
18
+ description: "Initialize a REPL implementation loop from a plan. Parses plan tasks, " +
19
+ "auto-detects build/test commands, and creates .cortex/repl-state.json " +
20
+ "for tracking progress through each task iteratively.",
21
+ args: {
22
+ planFilename: tool.schema
23
+ .string()
24
+ .describe("Plan filename from .cortex/plans/ to load tasks from"),
25
+ buildCommand: tool.schema
26
+ .string()
27
+ .optional()
28
+ .describe("Override auto-detected build command (e.g., 'npm run build')"),
29
+ testCommand: tool.schema
30
+ .string()
31
+ .optional()
32
+ .describe("Override auto-detected test command (e.g., 'npm test')"),
33
+ maxRetries: tool.schema
34
+ .number()
35
+ .optional()
36
+ .describe("Max retries per failing task before escalating to user (default: 3)"),
37
+ },
38
+ async execute(args, context) {
39
+ const { planFilename, buildCommand, testCommand, maxRetries = 3 } = args;
40
+ const cwd = context.worktree;
41
+ // 1. Validate plan filename
42
+ if (!planFilename || planFilename === "." || planFilename === "..") {
43
+ return `\u2717 Error: Invalid plan filename.`;
44
+ }
45
+ const plansDir = path.join(cwd, CORTEX_DIR, PLANS_DIR);
46
+ const planPath = path.resolve(plansDir, planFilename);
47
+ const resolvedPlansDir = path.resolve(plansDir);
48
+ // Prevent path traversal — resolved path must be strictly inside plans dir
49
+ if (!planPath.startsWith(resolvedPlansDir + path.sep)) {
50
+ return `\u2717 Error: Invalid plan filename.`;
51
+ }
52
+ if (!fs.existsSync(planPath)) {
53
+ return `\u2717 Error: Plan not found: ${planFilename}\n\nUse plan_list to see available plans.`;
54
+ }
55
+ const planContent = fs.readFileSync(planPath, "utf-8");
56
+ // 2. Parse tasks from plan
57
+ const taskDescriptions = parseTasksFromPlan(planContent);
58
+ if (taskDescriptions.length === 0) {
59
+ return `\u2717 Error: No tasks found in plan: ${planFilename}\n\nThe plan must contain unchecked checkbox items (- [ ] ...) in a ## Tasks section.`;
60
+ }
61
+ // 3. Auto-detect commands (or use overrides)
62
+ const detected = await detectCommands(cwd);
63
+ const finalBuild = buildCommand ?? detected.buildCommand;
64
+ const finalTest = testCommand ?? detected.testCommand;
65
+ // 4. Build initial state
66
+ const tasks = taskDescriptions.map((desc, i) => ({
67
+ index: i,
68
+ description: desc,
69
+ status: "pending",
70
+ retries: 0,
71
+ iterations: [],
72
+ }));
73
+ const state = {
74
+ planFilename,
75
+ startedAt: new Date().toISOString(),
76
+ buildCommand: finalBuild,
77
+ testCommand: finalTest,
78
+ lintCommand: detected.lintCommand,
79
+ maxRetries,
80
+ currentTaskIndex: -1,
81
+ tasks,
82
+ };
83
+ // 5. Write state
84
+ writeReplState(cwd, state);
85
+ // 6. Format output
86
+ const cmdInfo = detected.detected
87
+ ? `Auto-detected (${detected.framework})`
88
+ : "Not detected \u2014 provide overrides if needed";
89
+ return `\u2713 REPL loop initialized
90
+
91
+ Plan: ${planFilename}
92
+ Tasks: ${tasks.length}
93
+ Detection: ${cmdInfo}
94
+
95
+ Build: ${finalBuild || "(none)"}
96
+ Test: ${finalTest || "(none)"}
97
+ ${detected.lintCommand ? `Lint: ${detected.lintCommand}` : ""}
98
+ Max retries: ${maxRetries}
99
+
100
+ First task (#1):
101
+ "${tasks[0].description}"
102
+
103
+ Run \`repl_status\` to begin, then implement the task and run build/tests.`;
104
+ },
105
+ });
106
+ // ─── repl_status ─────────────────────────────────────────────────────────────
107
+ export const status = tool({
108
+ description: "Get the current REPL loop progress \u2014 which task is active, " +
109
+ "what\u2019s been completed, retry counts, and detected build/test commands. " +
110
+ "Call this to decide what to implement next.",
111
+ args: {},
112
+ async execute(args, context) {
113
+ const state = readReplState(context.worktree);
114
+ if (!state) {
115
+ return `\u2717 No REPL loop active.\n\nRun repl_init with a plan filename to start a loop.`;
116
+ }
117
+ // Auto-advance: if no task is in_progress, promote the next pending task
118
+ const current = getCurrentTask(state);
119
+ if (!current && !isLoopComplete(state)) {
120
+ const next = getNextTask(state);
121
+ if (next) {
122
+ next.status = "in_progress";
123
+ state.currentTaskIndex = next.index;
124
+ writeReplState(context.worktree, state);
125
+ }
126
+ }
127
+ return `\u2713 REPL Loop Status\n\n${formatProgress(state)}`;
128
+ },
129
+ });
130
+ // ─── repl_report ─────────────────────────────────────────────────────────────
131
+ export const report = tool({
132
+ description: "Report the outcome of the current task iteration. " +
133
+ "After implementing a task and running build/tests, report whether it passed, " +
134
+ "failed, or should be skipped. The loop will auto-advance on pass, " +
135
+ "retry on fail (up to max), or escalate to user when retries exhausted.",
136
+ args: {
137
+ result: tool.schema
138
+ .enum(["pass", "fail", "skip"])
139
+ .describe("Task result: 'pass' (build+tests green), 'fail' (something broke), 'skip' (defer task)"),
140
+ detail: tool.schema
141
+ .string()
142
+ .describe("Result details: test output summary, error message, or skip reason"),
143
+ },
144
+ async execute(args, context) {
145
+ const { result, detail } = args;
146
+ const state = readReplState(context.worktree);
147
+ if (!state) {
148
+ return `\u2717 No REPL loop active. Run repl_init first.`;
149
+ }
150
+ // Find the current in_progress task
151
+ const current = getCurrentTask(state);
152
+ if (!current) {
153
+ // Try to find the task at currentTaskIndex
154
+ if (state.currentTaskIndex >= 0 && state.currentTaskIndex < state.tasks.length) {
155
+ const task = state.tasks[state.currentTaskIndex];
156
+ if (task.status === "pending") {
157
+ task.status = "in_progress";
158
+ }
159
+ return processReport(state, task, result, detail, context.worktree);
160
+ }
161
+ return `\u2717 No task is currently in progress.\n\nRun repl_status to advance to the next task.`;
162
+ }
163
+ return processReport(state, current, result, detail, context.worktree);
164
+ },
165
+ });
166
+ /**
167
+ * Process a report for a task and update state.
168
+ */
169
+ function processReport(state, task, result, detail, cwd) {
170
+ // Record iteration
171
+ task.iterations.push({
172
+ at: new Date().toISOString(),
173
+ result,
174
+ detail: detail.substring(0, 2000), // Cap detail length
175
+ });
176
+ const taskNum = task.index + 1;
177
+ const taskDesc = task.description;
178
+ let output;
179
+ switch (result) {
180
+ case "pass": {
181
+ task.status = "passed";
182
+ const attempt = task.iterations.length;
183
+ const suffix = attempt === 1 ? "1st" : attempt === 2 ? "2nd" : attempt === 3 ? "3rd" : `${attempt}th`;
184
+ output = `\u2713 Task #${taskNum} PASSED (${suffix} attempt)\n "${taskDesc}"\n Detail: ${detail.substring(0, 200)}`;
185
+ break;
186
+ }
187
+ case "fail": {
188
+ task.retries += 1;
189
+ const attempt = task.iterations.length;
190
+ if (task.retries >= state.maxRetries) {
191
+ // Retries exhausted
192
+ task.status = "failed";
193
+ output = `\u2717 Task #${taskNum} FAILED \u2014 retries exhausted (${attempt}/${state.maxRetries} attempts)\n "${taskDesc}"\n Detail: ${detail.substring(0, 200)}\n\n\u2192 ASK THE USER how to proceed. Suggest: fix manually, skip task, or abort loop.`;
194
+ }
195
+ else {
196
+ // Retries remaining — stay in_progress
197
+ const remaining = state.maxRetries - task.retries;
198
+ output = `\u26A0 Task #${taskNum} FAILED (attempt ${attempt}/${state.maxRetries})\n "${taskDesc}"\n Detail: ${detail.substring(0, 200)}\n\n\u2192 Fix the issue and run build/tests again. ${remaining} retr${remaining > 1 ? "ies" : "y"} remaining.`;
199
+ // Don't advance — keep task in_progress
200
+ writeReplState(cwd, state);
201
+ return output;
202
+ }
203
+ break;
204
+ }
205
+ case "skip": {
206
+ task.status = "skipped";
207
+ output = `\u2298 Task #${taskNum} SKIPPED\n "${taskDesc}"\n Reason: ${detail.substring(0, 200)}`;
208
+ break;
209
+ }
210
+ }
211
+ // Advance to next task
212
+ const next = getNextTask(state);
213
+ if (next) {
214
+ next.status = "in_progress";
215
+ state.currentTaskIndex = next.index;
216
+ output += `\n\n\u2192 Next: Task #${next.index + 1} "${next.description}"`;
217
+ }
218
+ else {
219
+ // All tasks done
220
+ state.currentTaskIndex = -1;
221
+ state.completedAt = new Date().toISOString();
222
+ output += "\n\n\u2713 All tasks complete. Run repl_summary to generate the results report, then proceed to the quality gate (Step 7).";
223
+ }
224
+ writeReplState(cwd, state);
225
+ return output;
226
+ }
227
+ // ─── repl_summary ────────────────────────────────────────────────────────────
228
+ export const summary = tool({
229
+ description: "Generate a formatted summary of the REPL loop results for inclusion in " +
230
+ "the quality gate report and PR body. Call after all tasks are complete " +
231
+ "or when the loop is terminated.",
232
+ args: {},
233
+ async execute(args, context) {
234
+ const state = readReplState(context.worktree);
235
+ if (!state) {
236
+ return `\u2717 No REPL loop data found.\n\nRun repl_init to start a loop, or this may have been cleaned up already.`;
237
+ }
238
+ return formatSummary(state);
239
+ },
240
+ });
@@ -7,6 +7,7 @@ export declare const finalize: {
7
7
  baseBranch: import("zod").ZodOptional<import("zod").ZodString>;
8
8
  planFilename: import("zod").ZodOptional<import("zod").ZodString>;
9
9
  draft: import("zod").ZodOptional<import("zod").ZodBoolean>;
10
+ issueRefs: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodNumber>>;
10
11
  };
11
12
  execute(args: {
12
13
  commitMessage: string;
@@ -15,6 +16,7 @@ export declare const finalize: {
15
16
  baseBranch?: string | undefined;
16
17
  planFilename?: string | undefined;
17
18
  draft?: boolean | undefined;
19
+ issueRefs?: number[] | undefined;
18
20
  }, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
19
21
  };
20
22
  //# sourceMappingURL=task.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"task.d.ts","sourceRoot":"","sources":["../../src/tools/task.ts"],"names":[],"mappings":"AAyGA,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;CAqNnB,CAAC"}
1
+ {"version":3,"file":"task.d.ts","sourceRoot":"","sources":["../../src/tools/task.ts"],"names":[],"mappings":"AA+EA,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;CA0OnB,CAAC"}