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,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality Gate Summary Tool
|
|
3
|
+
*
|
|
4
|
+
* Aggregates sub-agent findings into a unified report with severity sorting,
|
|
5
|
+
* go/no-go recommendation, and PR body content.
|
|
6
|
+
*/
|
|
7
|
+
import { tool } from "@opencode-ai/plugin";
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
const CORTEX_DIR = ".cortex";
|
|
11
|
+
const QUALITY_GATE_FILE = "quality-gate.json";
|
|
12
|
+
// ─── Severity ordering ───────────────────────────────────────────────────────
|
|
13
|
+
const SEVERITY_ORDER = {
|
|
14
|
+
CRITICAL: 0,
|
|
15
|
+
HIGH: 1,
|
|
16
|
+
MEDIUM: 2,
|
|
17
|
+
LOW: 3,
|
|
18
|
+
INFO: 4,
|
|
19
|
+
};
|
|
20
|
+
// ─── quality_gate_summary ────────────────────────────────────────────────────
|
|
21
|
+
export const qualityGateSummary = tool({
|
|
22
|
+
description: "Aggregate sub-agent quality gate findings into a unified report. " +
|
|
23
|
+
"Parses findings from @testing, @security, @audit, @perf, @devops, and @docs-writer, " +
|
|
24
|
+
"sorts by severity, provides a go/no-go recommendation, and generates PR body content. " +
|
|
25
|
+
"Pass each agent's raw report text as a separate entry.",
|
|
26
|
+
args: {
|
|
27
|
+
scope: tool.schema
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("Change scope classification: trivial, low, standard, high"),
|
|
31
|
+
testing: tool.schema
|
|
32
|
+
.string()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Raw report from @testing sub-agent"),
|
|
35
|
+
security: tool.schema
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("Raw report from @security sub-agent"),
|
|
39
|
+
audit: tool.schema
|
|
40
|
+
.string()
|
|
41
|
+
.optional()
|
|
42
|
+
.describe("Raw report from @audit sub-agent"),
|
|
43
|
+
perf: tool.schema
|
|
44
|
+
.string()
|
|
45
|
+
.optional()
|
|
46
|
+
.describe("Raw report from @perf sub-agent"),
|
|
47
|
+
devops: tool.schema
|
|
48
|
+
.string()
|
|
49
|
+
.optional()
|
|
50
|
+
.describe("Raw report from @devops sub-agent"),
|
|
51
|
+
docsWriter: tool.schema
|
|
52
|
+
.string()
|
|
53
|
+
.optional()
|
|
54
|
+
.describe("Raw report from @docs-writer sub-agent"),
|
|
55
|
+
},
|
|
56
|
+
async execute(args, context) {
|
|
57
|
+
const cwd = context.worktree;
|
|
58
|
+
const reports = [];
|
|
59
|
+
// Parse each provided report
|
|
60
|
+
const agentEntries = [
|
|
61
|
+
["testing", args.testing],
|
|
62
|
+
["security", args.security],
|
|
63
|
+
["audit", args.audit],
|
|
64
|
+
["perf", args.perf],
|
|
65
|
+
["devops", args.devops],
|
|
66
|
+
["docs-writer", args.docsWriter],
|
|
67
|
+
];
|
|
68
|
+
for (const [agent, raw] of agentEntries) {
|
|
69
|
+
if (!raw)
|
|
70
|
+
continue;
|
|
71
|
+
reports.push(parseReport(agent, raw));
|
|
72
|
+
}
|
|
73
|
+
if (reports.length === 0) {
|
|
74
|
+
return `\u2717 No sub-agent reports provided. Pass at least one agent report to aggregate.`;
|
|
75
|
+
}
|
|
76
|
+
// Collect all findings and sort by severity
|
|
77
|
+
const allFindings = reports.flatMap((r) => r.findings);
|
|
78
|
+
allFindings.sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 99) - (SEVERITY_ORDER[b.severity] ?? 99));
|
|
79
|
+
// Determine recommendation
|
|
80
|
+
const hasCritical = allFindings.some((f) => f.severity === "CRITICAL");
|
|
81
|
+
const hasHigh = allFindings.some((f) => f.severity === "HIGH");
|
|
82
|
+
const hasMedium = allFindings.some((f) => f.severity === "MEDIUM");
|
|
83
|
+
let recommendation;
|
|
84
|
+
if (hasCritical || hasHigh) {
|
|
85
|
+
recommendation = "NO-GO";
|
|
86
|
+
}
|
|
87
|
+
else if (hasMedium) {
|
|
88
|
+
recommendation = "GO-WITH-WARNINGS";
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
recommendation = "GO";
|
|
92
|
+
}
|
|
93
|
+
// Build state for persistence
|
|
94
|
+
const state = {
|
|
95
|
+
timestamp: new Date().toISOString(),
|
|
96
|
+
scope: args.scope ?? "unknown",
|
|
97
|
+
reports,
|
|
98
|
+
recommendation,
|
|
99
|
+
};
|
|
100
|
+
// Persist state
|
|
101
|
+
persistState(cwd, state);
|
|
102
|
+
// Format output
|
|
103
|
+
const lines = [];
|
|
104
|
+
lines.push(`\u2713 Quality Gate Summary`);
|
|
105
|
+
lines.push("");
|
|
106
|
+
lines.push(`**Recommendation: ${recommendation}**`);
|
|
107
|
+
lines.push(`Scope: ${args.scope ?? "unknown"}`);
|
|
108
|
+
lines.push(`Agents: ${reports.map((r) => r.agent).join(", ")}`);
|
|
109
|
+
lines.push(`Total findings: ${allFindings.length}`);
|
|
110
|
+
lines.push("");
|
|
111
|
+
// Per-agent verdicts
|
|
112
|
+
lines.push("## Agent Results");
|
|
113
|
+
for (const report of reports) {
|
|
114
|
+
const agentFindings = report.findings;
|
|
115
|
+
const counts = countBySeverity(agentFindings);
|
|
116
|
+
lines.push(`- **@${report.agent}**: ${report.verdict} — ${formatCounts(counts)}`);
|
|
117
|
+
}
|
|
118
|
+
// All findings sorted by severity
|
|
119
|
+
if (allFindings.length > 0) {
|
|
120
|
+
lines.push("");
|
|
121
|
+
lines.push("## Findings (sorted by severity)");
|
|
122
|
+
for (const finding of allFindings) {
|
|
123
|
+
const loc = finding.location ? ` (${finding.location})` : "";
|
|
124
|
+
lines.push(`- **[${finding.severity}]** @${finding.agent}: ${finding.title}${loc}`);
|
|
125
|
+
if (finding.description) {
|
|
126
|
+
lines.push(` ${finding.description.substring(0, 200)}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Blockers
|
|
131
|
+
const blockers = allFindings.filter((f) => f.severity === "CRITICAL" || f.severity === "HIGH");
|
|
132
|
+
if (blockers.length > 0) {
|
|
133
|
+
lines.push("");
|
|
134
|
+
lines.push("## Blockers (must fix before merge)");
|
|
135
|
+
for (const b of blockers) {
|
|
136
|
+
lines.push(`- **[${b.severity}]** @${b.agent}: ${b.title}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// PR body section
|
|
140
|
+
lines.push("");
|
|
141
|
+
lines.push("## PR Body Quality Gate Section");
|
|
142
|
+
lines.push("```");
|
|
143
|
+
lines.push("## Quality Gate");
|
|
144
|
+
for (const report of reports) {
|
|
145
|
+
const counts = countBySeverity(report.findings);
|
|
146
|
+
lines.push(`- ${capitalize(report.agent)}: ${report.verdict} — ${formatCounts(counts)}`);
|
|
147
|
+
}
|
|
148
|
+
lines.push(`- **Recommendation: ${recommendation}**`);
|
|
149
|
+
lines.push("```");
|
|
150
|
+
return lines.join("\n");
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
// ─── Parsing helpers ─────────────────────────────────────────────────────────
|
|
154
|
+
function parseReport(agent, raw) {
|
|
155
|
+
const findings = [];
|
|
156
|
+
// Extract verdict
|
|
157
|
+
let verdict = "Unknown";
|
|
158
|
+
const verdictMatch = raw.match(/\*\*Verdict\*\*:\s*(.+)/i)
|
|
159
|
+
?? raw.match(/Verdict:\s*(.+)/i);
|
|
160
|
+
if (verdictMatch) {
|
|
161
|
+
verdict = verdictMatch[1].trim();
|
|
162
|
+
}
|
|
163
|
+
// Extract findings by severity markers
|
|
164
|
+
const findingRegex = /####?\s*\[(CRITICAL|HIGH|MEDIUM|LOW|INFO|BLOCKING|WARNING|ERROR|SUGGESTION|NITPICK|PRAISE)\]\s*(.+)/gi;
|
|
165
|
+
let match;
|
|
166
|
+
while ((match = findingRegex.exec(raw)) !== null) {
|
|
167
|
+
const rawSeverity = match[1].toUpperCase();
|
|
168
|
+
const title = match[2].trim();
|
|
169
|
+
// Normalize severity
|
|
170
|
+
let severity;
|
|
171
|
+
switch (rawSeverity) {
|
|
172
|
+
case "CRITICAL":
|
|
173
|
+
case "BLOCKING":
|
|
174
|
+
case "ERROR":
|
|
175
|
+
severity = "CRITICAL";
|
|
176
|
+
break;
|
|
177
|
+
case "HIGH":
|
|
178
|
+
severity = "HIGH";
|
|
179
|
+
break;
|
|
180
|
+
case "MEDIUM":
|
|
181
|
+
case "WARNING":
|
|
182
|
+
case "SUGGESTION":
|
|
183
|
+
severity = "MEDIUM";
|
|
184
|
+
break;
|
|
185
|
+
case "LOW":
|
|
186
|
+
case "NITPICK":
|
|
187
|
+
severity = "LOW";
|
|
188
|
+
break;
|
|
189
|
+
default:
|
|
190
|
+
severity = "INFO";
|
|
191
|
+
}
|
|
192
|
+
// Try to extract location from lines following the finding
|
|
193
|
+
const afterMatch = raw.substring(match.index + match[0].length, match.index + match[0].length + 500);
|
|
194
|
+
const locationMatch = afterMatch.match(/\*\*Location\*\*:\s*`?([^`\n]+)/i)
|
|
195
|
+
?? afterMatch.match(/\*\*File\*\*:\s*`?([^`\n]+)/i);
|
|
196
|
+
const descMatch = afterMatch.match(/\*\*Description\*\*:\s*(.+)/i);
|
|
197
|
+
findings.push({
|
|
198
|
+
agent,
|
|
199
|
+
severity,
|
|
200
|
+
title,
|
|
201
|
+
location: locationMatch?.[1]?.trim(),
|
|
202
|
+
description: descMatch?.[1]?.trim() ?? "",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return { agent, verdict, findings, raw };
|
|
206
|
+
}
|
|
207
|
+
function countBySeverity(findings) {
|
|
208
|
+
const counts = {};
|
|
209
|
+
for (const f of findings) {
|
|
210
|
+
counts[f.severity] = (counts[f.severity] ?? 0) + 1;
|
|
211
|
+
}
|
|
212
|
+
return counts;
|
|
213
|
+
}
|
|
214
|
+
function formatCounts(counts) {
|
|
215
|
+
const parts = [];
|
|
216
|
+
for (const sev of ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]) {
|
|
217
|
+
if (counts[sev]) {
|
|
218
|
+
parts.push(`${counts[sev]} ${sev.toLowerCase()}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return parts.length > 0 ? parts.join(", ") : "no findings";
|
|
222
|
+
}
|
|
223
|
+
function capitalize(s) {
|
|
224
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
225
|
+
}
|
|
226
|
+
function persistState(cwd, state) {
|
|
227
|
+
const dir = path.join(cwd, CORTEX_DIR);
|
|
228
|
+
if (!fs.existsSync(dir)) {
|
|
229
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
230
|
+
}
|
|
231
|
+
const filepath = path.join(dir, QUALITY_GATE_FILE);
|
|
232
|
+
fs.writeFileSync(filepath, JSON.stringify(state, null, 2));
|
|
233
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
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 resume: {
|
|
46
|
+
description: string;
|
|
47
|
+
args: {};
|
|
48
|
+
execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
49
|
+
};
|
|
50
|
+
export declare const summary: {
|
|
51
|
+
description: string;
|
|
52
|
+
args: {};
|
|
53
|
+
execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
54
|
+
};
|
|
55
|
+
//# 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;AA0BH,eAAO,MAAM,IAAI;;;;;;;;;;;;;;CAwGf,CAAC;AAIH,eAAO,MAAM,MAAM;;;;CA0BjB,CAAC;AAIH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;CAwCjB,CAAC;AAkFH,eAAO,MAAM,MAAM;;;;CA+CjB,CAAC;AAIH,eAAO,MAAM,OAAO;;;;CAclB,CAAC"}
|
|
@@ -0,0 +1,291 @@
|
|
|
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 { parseTasksWithAC, detectCommands, readCortexConfig, readReplState, writeReplState, getNextTask, getCurrentTask, isLoopComplete, detectIncompleteState, 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 cwd = context.worktree;
|
|
40
|
+
const config = readCortexConfig(cwd);
|
|
41
|
+
const { planFilename, buildCommand, testCommand, maxRetries = config.maxRetries ?? 3 } = args;
|
|
42
|
+
// 1. Validate plan filename
|
|
43
|
+
if (!planFilename || planFilename === "." || planFilename === "..") {
|
|
44
|
+
return `\u2717 Error: Invalid plan filename.`;
|
|
45
|
+
}
|
|
46
|
+
const plansDir = path.join(cwd, CORTEX_DIR, PLANS_DIR);
|
|
47
|
+
const planPath = path.resolve(plansDir, planFilename);
|
|
48
|
+
const resolvedPlansDir = path.resolve(plansDir);
|
|
49
|
+
// Prevent path traversal — resolved path must be strictly inside plans dir
|
|
50
|
+
if (!planPath.startsWith(resolvedPlansDir + path.sep)) {
|
|
51
|
+
return `\u2717 Error: Invalid plan filename.`;
|
|
52
|
+
}
|
|
53
|
+
if (!fs.existsSync(planPath)) {
|
|
54
|
+
return `\u2717 Error: Plan not found: ${planFilename}\n\nUse plan_list to see available plans.`;
|
|
55
|
+
}
|
|
56
|
+
const planContent = fs.readFileSync(planPath, "utf-8");
|
|
57
|
+
// 2. Parse tasks from plan (with acceptance criteria)
|
|
58
|
+
const parsedTasks = parseTasksWithAC(planContent);
|
|
59
|
+
if (parsedTasks.length === 0) {
|
|
60
|
+
return `\u2717 Error: No tasks found in plan: ${planFilename}\n\nThe plan must contain unchecked checkbox items (- [ ] ...) in a ## Tasks section.`;
|
|
61
|
+
}
|
|
62
|
+
// 3. Auto-detect commands (or use overrides)
|
|
63
|
+
const detected = await detectCommands(cwd);
|
|
64
|
+
const finalBuild = buildCommand ?? detected.buildCommand;
|
|
65
|
+
const finalTest = testCommand ?? detected.testCommand;
|
|
66
|
+
// 4. Build initial state
|
|
67
|
+
const tasks = parsedTasks.map((parsed, i) => ({
|
|
68
|
+
index: i,
|
|
69
|
+
description: parsed.description,
|
|
70
|
+
acceptanceCriteria: parsed.acceptanceCriteria,
|
|
71
|
+
status: "pending",
|
|
72
|
+
retries: 0,
|
|
73
|
+
iterations: [],
|
|
74
|
+
}));
|
|
75
|
+
const state = {
|
|
76
|
+
planFilename,
|
|
77
|
+
startedAt: new Date().toISOString(),
|
|
78
|
+
buildCommand: finalBuild,
|
|
79
|
+
testCommand: finalTest,
|
|
80
|
+
lintCommand: detected.lintCommand,
|
|
81
|
+
maxRetries,
|
|
82
|
+
currentTaskIndex: -1,
|
|
83
|
+
tasks,
|
|
84
|
+
};
|
|
85
|
+
// 5. Write state
|
|
86
|
+
writeReplState(cwd, state);
|
|
87
|
+
// 6. Format output
|
|
88
|
+
const cmdInfo = detected.detected
|
|
89
|
+
? `Auto-detected (${detected.framework})`
|
|
90
|
+
: "Not detected \u2014 provide overrides if needed";
|
|
91
|
+
return `\u2713 REPL loop initialized
|
|
92
|
+
|
|
93
|
+
Plan: ${planFilename}
|
|
94
|
+
Tasks: ${tasks.length}
|
|
95
|
+
Detection: ${cmdInfo}
|
|
96
|
+
|
|
97
|
+
Build: ${finalBuild || "(none)"}
|
|
98
|
+
Test: ${finalTest || "(none)"}
|
|
99
|
+
${detected.lintCommand ? `Lint: ${detected.lintCommand}` : ""}
|
|
100
|
+
Max retries: ${maxRetries}
|
|
101
|
+
|
|
102
|
+
First task (#1):
|
|
103
|
+
"${tasks[0].description}"
|
|
104
|
+
|
|
105
|
+
Run \`repl_status\` to begin, then implement the task and run build/tests.`;
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
// ─── repl_status ─────────────────────────────────────────────────────────────
|
|
109
|
+
export const status = tool({
|
|
110
|
+
description: "Get the current REPL loop progress \u2014 which task is active, " +
|
|
111
|
+
"what\u2019s been completed, retry counts, and detected build/test commands. " +
|
|
112
|
+
"Call this to decide what to implement next.",
|
|
113
|
+
args: {},
|
|
114
|
+
async execute(args, context) {
|
|
115
|
+
const state = readReplState(context.worktree);
|
|
116
|
+
if (!state) {
|
|
117
|
+
return `\u2717 No REPL loop active.\n\nRun repl_init with a plan filename to start a loop.`;
|
|
118
|
+
}
|
|
119
|
+
// Auto-advance: if no task is in_progress, promote the next pending task
|
|
120
|
+
const current = getCurrentTask(state);
|
|
121
|
+
if (!current && !isLoopComplete(state)) {
|
|
122
|
+
const next = getNextTask(state);
|
|
123
|
+
if (next) {
|
|
124
|
+
next.status = "in_progress";
|
|
125
|
+
next.startedAt = new Date().toISOString();
|
|
126
|
+
state.currentTaskIndex = next.index;
|
|
127
|
+
writeReplState(context.worktree, state);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return `\u2713 REPL Loop Status\n\n${formatProgress(state)}`;
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
// ─── repl_report ─────────────────────────────────────────────────────────────
|
|
134
|
+
export const report = tool({
|
|
135
|
+
description: "Report the outcome of the current task iteration. " +
|
|
136
|
+
"After implementing a task and running build/tests, report whether it passed, " +
|
|
137
|
+
"failed, or should be skipped. The loop will auto-advance on pass, " +
|
|
138
|
+
"retry on fail (up to max), or escalate to user when retries exhausted.",
|
|
139
|
+
args: {
|
|
140
|
+
result: tool.schema
|
|
141
|
+
.enum(["pass", "fail", "skip"])
|
|
142
|
+
.describe("Task result: 'pass' (build+tests green), 'fail' (something broke), 'skip' (defer task)"),
|
|
143
|
+
detail: tool.schema
|
|
144
|
+
.string()
|
|
145
|
+
.describe("Result details: test output summary, error message, or skip reason"),
|
|
146
|
+
},
|
|
147
|
+
async execute(args, context) {
|
|
148
|
+
const { result, detail } = args;
|
|
149
|
+
const state = readReplState(context.worktree);
|
|
150
|
+
if (!state) {
|
|
151
|
+
return `\u2717 No REPL loop active. Run repl_init first.`;
|
|
152
|
+
}
|
|
153
|
+
// Find the current in_progress task
|
|
154
|
+
const current = getCurrentTask(state);
|
|
155
|
+
if (!current) {
|
|
156
|
+
// Try to find the task at currentTaskIndex
|
|
157
|
+
if (state.currentTaskIndex >= 0 && state.currentTaskIndex < state.tasks.length) {
|
|
158
|
+
const task = state.tasks[state.currentTaskIndex];
|
|
159
|
+
if (task.status === "pending") {
|
|
160
|
+
task.status = "in_progress";
|
|
161
|
+
}
|
|
162
|
+
return processReport(state, task, result, detail, context.worktree);
|
|
163
|
+
}
|
|
164
|
+
return `\u2717 No task is currently in progress.\n\nRun repl_status to advance to the next task.`;
|
|
165
|
+
}
|
|
166
|
+
return processReport(state, current, result, detail, context.worktree);
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
/**
|
|
170
|
+
* Process a report for a task and update state.
|
|
171
|
+
*/
|
|
172
|
+
function processReport(state, task, result, detail, cwd) {
|
|
173
|
+
// Record iteration
|
|
174
|
+
task.iterations.push({
|
|
175
|
+
at: new Date().toISOString(),
|
|
176
|
+
result,
|
|
177
|
+
detail: detail.substring(0, 2000), // Cap detail length
|
|
178
|
+
});
|
|
179
|
+
const taskNum = task.index + 1;
|
|
180
|
+
const taskDesc = task.description;
|
|
181
|
+
let output;
|
|
182
|
+
switch (result) {
|
|
183
|
+
case "pass": {
|
|
184
|
+
task.status = "passed";
|
|
185
|
+
task.completedAt = new Date().toISOString();
|
|
186
|
+
const attempt = task.iterations.length;
|
|
187
|
+
const suffix = attempt === 1 ? "1st" : attempt === 2 ? "2nd" : attempt === 3 ? "3rd" : `${attempt}th`;
|
|
188
|
+
output = `\u2713 Task #${taskNum} PASSED (${suffix} attempt)\n "${taskDesc}"\n Detail: ${detail.substring(0, 200)}`;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
case "fail": {
|
|
192
|
+
task.retries += 1;
|
|
193
|
+
const attempt = task.iterations.length;
|
|
194
|
+
if (task.retries >= state.maxRetries) {
|
|
195
|
+
// Retries exhausted
|
|
196
|
+
task.status = "failed";
|
|
197
|
+
task.completedAt = new Date().toISOString();
|
|
198
|
+
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.`;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// Retries remaining — stay in_progress
|
|
202
|
+
const remaining = state.maxRetries - task.retries;
|
|
203
|
+
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.`;
|
|
204
|
+
// Don't advance — keep task in_progress
|
|
205
|
+
writeReplState(cwd, state);
|
|
206
|
+
return output;
|
|
207
|
+
}
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
case "skip": {
|
|
211
|
+
task.status = "skipped";
|
|
212
|
+
task.completedAt = new Date().toISOString();
|
|
213
|
+
output = `\u2298 Task #${taskNum} SKIPPED\n "${taskDesc}"\n Reason: ${detail.substring(0, 200)}`;
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Advance to next task
|
|
218
|
+
const next = getNextTask(state);
|
|
219
|
+
if (next) {
|
|
220
|
+
next.status = "in_progress";
|
|
221
|
+
next.startedAt = new Date().toISOString();
|
|
222
|
+
state.currentTaskIndex = next.index;
|
|
223
|
+
output += `\n\n\u2192 Next: Task #${next.index + 1} "${next.description}"`;
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
// All tasks done
|
|
227
|
+
state.currentTaskIndex = -1;
|
|
228
|
+
state.completedAt = new Date().toISOString();
|
|
229
|
+
output += "\n\n\u2713 All tasks complete. Run repl_summary to generate the results report, then proceed to the quality gate (Step 7).";
|
|
230
|
+
}
|
|
231
|
+
writeReplState(cwd, state);
|
|
232
|
+
return output;
|
|
233
|
+
}
|
|
234
|
+
// ─── repl_resume ─────────────────────────────────────────────────────────────
|
|
235
|
+
export const resume = tool({
|
|
236
|
+
description: "Detect and resume an interrupted REPL loop. Checks for incomplete state " +
|
|
237
|
+
"in .cortex/repl-state.json and offers to continue from where it left off. " +
|
|
238
|
+
"Call this at the start of a session to recover from crashes or context loss.",
|
|
239
|
+
args: {},
|
|
240
|
+
async execute(args, context) {
|
|
241
|
+
const state = detectIncompleteState(context.worktree);
|
|
242
|
+
if (!state) {
|
|
243
|
+
return `\u2717 No interrupted REPL loop found.\n\nEither no loop has been started, or the previous loop completed normally.`;
|
|
244
|
+
}
|
|
245
|
+
const total = state.tasks.length;
|
|
246
|
+
const passed = state.tasks.filter((t) => t.status === "passed").length;
|
|
247
|
+
const failed = state.tasks.filter((t) => t.status === "failed").length;
|
|
248
|
+
const skipped = state.tasks.filter((t) => t.status === "skipped").length;
|
|
249
|
+
const done = passed + failed + skipped;
|
|
250
|
+
const current = getCurrentTask(state);
|
|
251
|
+
const next = getNextTask(state);
|
|
252
|
+
const lines = [];
|
|
253
|
+
lines.push(`\u2713 Interrupted REPL loop detected`);
|
|
254
|
+
lines.push("");
|
|
255
|
+
lines.push(`Plan: ${state.planFilename}`);
|
|
256
|
+
lines.push(`Progress: ${done}/${total} tasks completed (${passed} passed, ${failed} failed, ${skipped} skipped)`);
|
|
257
|
+
lines.push(`Started: ${state.startedAt}`);
|
|
258
|
+
if (current) {
|
|
259
|
+
lines.push("");
|
|
260
|
+
lines.push(`Interrupted task (#${current.index + 1}):`);
|
|
261
|
+
lines.push(` "${current.description}"`);
|
|
262
|
+
lines.push(` Status: in_progress (${current.retries} retries so far)`);
|
|
263
|
+
if (current.iterations.length > 0) {
|
|
264
|
+
const lastIter = current.iterations[current.iterations.length - 1];
|
|
265
|
+
lines.push(` Last attempt: ${lastIter.result} — ${lastIter.detail.substring(0, 100)}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
else if (next) {
|
|
269
|
+
lines.push("");
|
|
270
|
+
lines.push(`Next pending task (#${next.index + 1}):`);
|
|
271
|
+
lines.push(` "${next.description}"`);
|
|
272
|
+
}
|
|
273
|
+
lines.push("");
|
|
274
|
+
lines.push(`\u2192 Run repl_status to continue the loop from where it left off.`);
|
|
275
|
+
return lines.join("\n");
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
// ─── repl_summary ────────────────────────────────────────────────────────────
|
|
279
|
+
export const summary = tool({
|
|
280
|
+
description: "Generate a formatted summary of the REPL loop results for inclusion in " +
|
|
281
|
+
"the quality gate report and PR body. Call after all tasks are complete " +
|
|
282
|
+
"or when the loop is terminated.",
|
|
283
|
+
args: {},
|
|
284
|
+
async execute(args, context) {
|
|
285
|
+
const state = readReplState(context.worktree);
|
|
286
|
+
if (!state) {
|
|
287
|
+
return `\u2717 No REPL loop data found.\n\nRun repl_init to start a loop, or this may have been cleaned up already.`;
|
|
288
|
+
}
|
|
289
|
+
return formatSummary(state);
|
|
290
|
+
},
|
|
291
|
+
});
|
package/dist/tools/task.d.ts
CHANGED
|
@@ -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
|
package/dist/tools/task.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task.d.ts","sourceRoot":"","sources":["../../src/tools/task.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"task.d.ts","sourceRoot":"","sources":["../../src/tools/task.ts"],"names":[],"mappings":"AA+EA,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;CA0OnB,CAAC"}
|