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,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
|
+
all: "all";
|
|
11
|
+
open: "open";
|
|
12
|
+
closed: "closed";
|
|
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?: "all" | "open" | "closed" | 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
|
+
});
|
package/dist/tools/plan.d.ts
CHANGED
|
@@ -1,32 +1,36 @@
|
|
|
1
|
+
import type { PluginInput } from "@opencode-ai/plugin";
|
|
2
|
+
type Client = PluginInput["client"];
|
|
1
3
|
export declare const save: {
|
|
2
4
|
description: string;
|
|
3
5
|
args: {
|
|
4
6
|
title: import("zod").ZodString;
|
|
5
7
|
type: import("zod").ZodEnum<{
|
|
8
|
+
refactor: "refactor";
|
|
6
9
|
feature: "feature";
|
|
7
10
|
bugfix: "bugfix";
|
|
8
|
-
refactor: "refactor";
|
|
9
11
|
spike: "spike";
|
|
10
12
|
docs: "docs";
|
|
11
13
|
architecture: "architecture";
|
|
12
14
|
}>;
|
|
13
15
|
content: import("zod").ZodString;
|
|
14
16
|
tasks: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
|
|
17
|
+
branch: import("zod").ZodOptional<import("zod").ZodString>;
|
|
15
18
|
};
|
|
16
19
|
execute(args: {
|
|
17
20
|
title: string;
|
|
18
|
-
type: "
|
|
21
|
+
type: "refactor" | "feature" | "bugfix" | "spike" | "docs" | "architecture";
|
|
19
22
|
content: string;
|
|
20
23
|
tasks?: string[] | undefined;
|
|
24
|
+
branch?: string | undefined;
|
|
21
25
|
}, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
22
26
|
};
|
|
23
27
|
export declare const list: {
|
|
24
28
|
description: string;
|
|
25
29
|
args: {
|
|
26
30
|
type: import("zod").ZodOptional<import("zod").ZodEnum<{
|
|
31
|
+
refactor: "refactor";
|
|
27
32
|
feature: "feature";
|
|
28
33
|
bugfix: "bugfix";
|
|
29
|
-
refactor: "refactor";
|
|
30
34
|
spike: "spike";
|
|
31
35
|
docs: "docs";
|
|
32
36
|
architecture: "architecture";
|
|
@@ -34,7 +38,7 @@ export declare const list: {
|
|
|
34
38
|
}>>;
|
|
35
39
|
};
|
|
36
40
|
execute(args: {
|
|
37
|
-
type?: "
|
|
41
|
+
type?: "refactor" | "feature" | "bugfix" | "spike" | "docs" | "architecture" | "all" | undefined;
|
|
38
42
|
}, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
39
43
|
};
|
|
40
44
|
export declare const load: {
|
|
@@ -55,5 +59,25 @@ export declare const delete_: {
|
|
|
55
59
|
filename: string;
|
|
56
60
|
}, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
57
61
|
};
|
|
62
|
+
/**
|
|
63
|
+
* Factory function that creates the plan_commit tool with access
|
|
64
|
+
* to the OpenCode client for toast notifications.
|
|
65
|
+
*
|
|
66
|
+
* Creates a git branch from plan metadata, stages .cortex/ artifacts,
|
|
67
|
+
* commits them, and writes the branch name back into the plan frontmatter.
|
|
68
|
+
*/
|
|
69
|
+
export declare function createCommit(client: Client): {
|
|
70
|
+
description: string;
|
|
71
|
+
args: {
|
|
72
|
+
planFilename: import("zod").ZodString;
|
|
73
|
+
branchType: import("zod").ZodOptional<import("zod").ZodString>;
|
|
74
|
+
branchName: import("zod").ZodOptional<import("zod").ZodString>;
|
|
75
|
+
};
|
|
76
|
+
execute(args: {
|
|
77
|
+
planFilename: string;
|
|
78
|
+
branchType?: string | undefined;
|
|
79
|
+
branchName?: string | undefined;
|
|
80
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
81
|
+
};
|
|
58
82
|
export { delete_ as delete };
|
|
59
83
|
//# sourceMappingURL=plan.d.ts.map
|
package/dist/tools/plan.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../src/tools/plan.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../src/tools/plan.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAgBvD,KAAK,MAAM,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;AA2BpC,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;CAgEf,CAAC;AAEH,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;CAkEf,CAAC;AAEH,eAAO,MAAM,IAAI;;;;;;;;CA6Bf,CAAC;AAEH,eAAO,MAAM,OAAO;;;;;;;;CAwBlB,CAAC;AAEH;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;;;EAsN1C;AAGD,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,CAAC"}
|
package/dist/tools/plan.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin";
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
|
+
import { git } from "../utils/shell.js";
|
|
5
|
+
import { TYPE_TO_PREFIX, parseFrontmatter, upsertFrontmatterField, } from "../utils/plan-extract.js";
|
|
4
6
|
const CORTEX_DIR = ".cortex";
|
|
5
7
|
const PLANS_DIR = "plans";
|
|
8
|
+
const PROTECTED_BRANCHES = ["main", "master", "develop", "production", "staging"];
|
|
6
9
|
function slugify(text) {
|
|
7
10
|
return text
|
|
8
11
|
.toLowerCase()
|
|
@@ -37,20 +40,25 @@ export const save = tool({
|
|
|
37
40
|
.array(tool.schema.string())
|
|
38
41
|
.optional()
|
|
39
42
|
.describe("Optional list of tasks"),
|
|
43
|
+
branch: tool.schema
|
|
44
|
+
.string()
|
|
45
|
+
.optional()
|
|
46
|
+
.describe("Optional branch name to store in frontmatter (set by plan_commit)"),
|
|
40
47
|
},
|
|
41
48
|
async execute(args, context) {
|
|
42
|
-
const { title, type, content, tasks } = args;
|
|
49
|
+
const { title, type, content, tasks, branch } = args;
|
|
43
50
|
const plansPath = ensureCortexDir(context.worktree);
|
|
44
51
|
const datePrefix = getDatePrefix();
|
|
45
52
|
const slug = slugify(title);
|
|
46
53
|
const filename = `${datePrefix}-${type}-${slug}.md`;
|
|
47
54
|
const filepath = path.join(plansPath, filename);
|
|
48
55
|
// Build frontmatter
|
|
56
|
+
const branchLine = branch ? `\nbranch: ${branch}` : "";
|
|
49
57
|
const frontmatter = `---
|
|
50
58
|
title: "${title}"
|
|
51
59
|
type: ${type}
|
|
52
60
|
created: ${new Date().toISOString()}
|
|
53
|
-
status: draft
|
|
61
|
+
status: draft${branchLine}
|
|
54
62
|
---
|
|
55
63
|
|
|
56
64
|
`;
|
|
@@ -144,7 +152,12 @@ export const load = tool({
|
|
|
144
152
|
async execute(args, context) {
|
|
145
153
|
const { filename } = args;
|
|
146
154
|
const plansPath = path.join(context.worktree, CORTEX_DIR, PLANS_DIR);
|
|
147
|
-
const filepath = path.
|
|
155
|
+
const filepath = path.resolve(plansPath, filename);
|
|
156
|
+
const resolvedPlansDir = path.resolve(plansPath);
|
|
157
|
+
// Prevent path traversal (../ or absolute paths)
|
|
158
|
+
if (!filepath.startsWith(resolvedPlansDir + path.sep)) {
|
|
159
|
+
return `✗ Invalid plan filename: path traversal not allowed`;
|
|
160
|
+
}
|
|
148
161
|
if (!fs.existsSync(filepath)) {
|
|
149
162
|
return `✗ Plan not found: ${filename}
|
|
150
163
|
|
|
@@ -165,7 +178,12 @@ export const delete_ = tool({
|
|
|
165
178
|
async execute(args, context) {
|
|
166
179
|
const { filename } = args;
|
|
167
180
|
const plansPath = path.join(context.worktree, CORTEX_DIR, PLANS_DIR);
|
|
168
|
-
const filepath = path.
|
|
181
|
+
const filepath = path.resolve(plansPath, filename);
|
|
182
|
+
const resolvedPlansDir = path.resolve(plansPath);
|
|
183
|
+
// Prevent path traversal (../ or absolute paths)
|
|
184
|
+
if (!filepath.startsWith(resolvedPlansDir + path.sep)) {
|
|
185
|
+
return `✗ Invalid plan filename: path traversal not allowed`;
|
|
186
|
+
}
|
|
169
187
|
if (!fs.existsSync(filepath)) {
|
|
170
188
|
return `✗ Plan not found: ${filename}`;
|
|
171
189
|
}
|
|
@@ -173,5 +191,215 @@ export const delete_ = tool({
|
|
|
173
191
|
return `✓ Deleted plan: ${filename}`;
|
|
174
192
|
},
|
|
175
193
|
});
|
|
194
|
+
/**
|
|
195
|
+
* Factory function that creates the plan_commit tool with access
|
|
196
|
+
* to the OpenCode client for toast notifications.
|
|
197
|
+
*
|
|
198
|
+
* Creates a git branch from plan metadata, stages .cortex/ artifacts,
|
|
199
|
+
* commits them, and writes the branch name back into the plan frontmatter.
|
|
200
|
+
*/
|
|
201
|
+
export function createCommit(client) {
|
|
202
|
+
return tool({
|
|
203
|
+
description: "Create a git branch from a saved plan, commit .cortex/ artifacts to it, and " +
|
|
204
|
+
"write the branch name into the plan frontmatter. Keeps main clean.",
|
|
205
|
+
args: {
|
|
206
|
+
planFilename: tool.schema
|
|
207
|
+
.string()
|
|
208
|
+
.describe("Plan filename from .cortex/plans/ (e.g., '2026-02-26-feature-auth.md')"),
|
|
209
|
+
branchType: tool.schema
|
|
210
|
+
.string()
|
|
211
|
+
.optional()
|
|
212
|
+
.describe("Override branch prefix (default: derived from plan type)"),
|
|
213
|
+
branchName: tool.schema
|
|
214
|
+
.string()
|
|
215
|
+
.optional()
|
|
216
|
+
.describe("Override branch slug (default: derived from plan title)"),
|
|
217
|
+
},
|
|
218
|
+
async execute(args, context) {
|
|
219
|
+
const { planFilename, branchType, branchName: nameOverride } = args;
|
|
220
|
+
const cwd = context.worktree;
|
|
221
|
+
// ── 1. Validate: git repo ─────────────────────────────────
|
|
222
|
+
try {
|
|
223
|
+
await git(cwd, "rev-parse", "--git-dir");
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return "✗ Error: Not in a git repository. Initialize git first.";
|
|
227
|
+
}
|
|
228
|
+
// ── 2. Read and parse the plan ────────────────────────────
|
|
229
|
+
const plansPath = path.join(cwd, CORTEX_DIR, PLANS_DIR);
|
|
230
|
+
const filepath = path.resolve(plansPath, planFilename);
|
|
231
|
+
const resolvedPlansDir = path.resolve(plansPath);
|
|
232
|
+
// Prevent path traversal (../ or absolute paths)
|
|
233
|
+
if (!filepath.startsWith(resolvedPlansDir + path.sep)) {
|
|
234
|
+
return `✗ Invalid plan filename: path traversal not allowed`;
|
|
235
|
+
}
|
|
236
|
+
if (!fs.existsSync(filepath)) {
|
|
237
|
+
return `✗ Plan not found: ${planFilename}
|
|
238
|
+
|
|
239
|
+
Use plan_list to see available plans.`;
|
|
240
|
+
}
|
|
241
|
+
let planContent = fs.readFileSync(filepath, "utf-8");
|
|
242
|
+
const fm = parseFrontmatter(planContent);
|
|
243
|
+
if (!fm) {
|
|
244
|
+
return `✗ Plan has no frontmatter: ${planFilename}
|
|
245
|
+
|
|
246
|
+
Expected YAML frontmatter with title and type fields.`;
|
|
247
|
+
}
|
|
248
|
+
const planTitle = fm.title || "untitled";
|
|
249
|
+
const planType = fm.type || "feature";
|
|
250
|
+
// ── 3. Determine branch name ──────────────────────────────
|
|
251
|
+
const VALID_PREFIXES = Object.values(TYPE_TO_PREFIX);
|
|
252
|
+
const rawPrefix = branchType || TYPE_TO_PREFIX[planType] || "feature";
|
|
253
|
+
const prefix = VALID_PREFIXES.includes(rawPrefix) ? rawPrefix : "feature";
|
|
254
|
+
// Always sanitize name through slugify (even overrides)
|
|
255
|
+
const slug = slugify(nameOverride || planTitle);
|
|
256
|
+
const fullBranchName = `${prefix}/${slug}`;
|
|
257
|
+
// ── 4. Check current branch ───────────────────────────────
|
|
258
|
+
let currentBranch = "";
|
|
259
|
+
try {
|
|
260
|
+
const { stdout } = await git(cwd, "branch", "--show-current");
|
|
261
|
+
currentBranch = stdout.trim();
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
currentBranch = "";
|
|
265
|
+
}
|
|
266
|
+
const isOnProtected = PROTECTED_BRANCHES.includes(currentBranch);
|
|
267
|
+
let branchCreated = false;
|
|
268
|
+
// ── 5. Create or switch to branch ─────────────────────────
|
|
269
|
+
if (isOnProtected || !currentBranch) {
|
|
270
|
+
// Try to create the branch
|
|
271
|
+
try {
|
|
272
|
+
await git(cwd, "checkout", "-b", fullBranchName);
|
|
273
|
+
branchCreated = true;
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
// Branch may already exist — try switching to it
|
|
277
|
+
try {
|
|
278
|
+
await git(cwd, "checkout", fullBranchName);
|
|
279
|
+
}
|
|
280
|
+
catch (switchErr) {
|
|
281
|
+
try {
|
|
282
|
+
await client.tui.showToast({
|
|
283
|
+
body: {
|
|
284
|
+
title: `Plan Commit: ${planFilename}`,
|
|
285
|
+
message: `Failed to create/switch branch: ${switchErr.message || switchErr}`,
|
|
286
|
+
variant: "error",
|
|
287
|
+
duration: 8000,
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
// Toast failure is non-fatal
|
|
293
|
+
}
|
|
294
|
+
return `✗ Error creating branch '${fullBranchName}': ${switchErr.message || switchErr}
|
|
295
|
+
|
|
296
|
+
You may have uncommitted changes. Commit or stash them first.`;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// If already on a non-protected branch, skip branch creation
|
|
301
|
+
// ── 6. Update plan frontmatter with branch ────────────────
|
|
302
|
+
// If we created/switched to a new branch → use that name
|
|
303
|
+
// If already on a non-protected branch → use whatever we're on
|
|
304
|
+
const targetBranch = (isOnProtected || branchCreated) ? fullBranchName : currentBranch;
|
|
305
|
+
planContent = upsertFrontmatterField(planContent, "branch", targetBranch);
|
|
306
|
+
fs.writeFileSync(filepath, planContent);
|
|
307
|
+
// ── 7. Stage .cortex/ directory ───────────────────────────
|
|
308
|
+
try {
|
|
309
|
+
await git(cwd, "add", path.join(cwd, CORTEX_DIR));
|
|
310
|
+
}
|
|
311
|
+
catch (stageErr) {
|
|
312
|
+
return `✗ Error staging .cortex/ directory: ${stageErr.message || stageErr}`;
|
|
313
|
+
}
|
|
314
|
+
// ── 8. Commit ─────────────────────────────────────────────
|
|
315
|
+
const commitMsg = `chore(plan): ${planTitle}`;
|
|
316
|
+
let commitHash = "";
|
|
317
|
+
try {
|
|
318
|
+
// Check if there's anything staged
|
|
319
|
+
const { stdout: statusOut } = await git(cwd, "status", "--porcelain");
|
|
320
|
+
const stagedLines = statusOut
|
|
321
|
+
.trim()
|
|
322
|
+
.split("\n")
|
|
323
|
+
.filter((l) => l && l[0] !== " " && l[0] !== "?");
|
|
324
|
+
if (stagedLines.length === 0) {
|
|
325
|
+
// Nothing to commit — plan already committed
|
|
326
|
+
try {
|
|
327
|
+
const { stdout: hashOut } = await git(cwd, "rev-parse", "--short", "HEAD");
|
|
328
|
+
commitHash = hashOut.trim();
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
commitHash = "(unknown)";
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
await client.tui.showToast({
|
|
335
|
+
body: {
|
|
336
|
+
title: `Plan: ${targetBranch}`,
|
|
337
|
+
message: "Already committed — no new changes",
|
|
338
|
+
variant: "info",
|
|
339
|
+
duration: 4000,
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
// Toast failure is non-fatal
|
|
345
|
+
}
|
|
346
|
+
return `✓ Plan already committed on branch: ${targetBranch}
|
|
347
|
+
|
|
348
|
+
Branch: ${targetBranch}
|
|
349
|
+
Commit: ${commitHash} (no new changes)
|
|
350
|
+
Plan: ${planFilename}
|
|
351
|
+
|
|
352
|
+
The plan branch is ready for implementation.
|
|
353
|
+
Use worktree_create or switch to the Implement agent.`;
|
|
354
|
+
}
|
|
355
|
+
await git(cwd, "commit", "-m", commitMsg);
|
|
356
|
+
const { stdout: hashOut } = await git(cwd, "rev-parse", "--short", "HEAD");
|
|
357
|
+
commitHash = hashOut.trim();
|
|
358
|
+
}
|
|
359
|
+
catch (commitErr) {
|
|
360
|
+
try {
|
|
361
|
+
await client.tui.showToast({
|
|
362
|
+
body: {
|
|
363
|
+
title: `Plan Commit`,
|
|
364
|
+
message: `Commit failed: ${commitErr.message || commitErr}`,
|
|
365
|
+
variant: "error",
|
|
366
|
+
duration: 8000,
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
// Toast failure is non-fatal
|
|
372
|
+
}
|
|
373
|
+
return `✗ Error committing: ${commitErr.message || commitErr}`;
|
|
374
|
+
}
|
|
375
|
+
// ── 9. Success notification ───────────────────────────────
|
|
376
|
+
try {
|
|
377
|
+
await client.tui.showToast({
|
|
378
|
+
body: {
|
|
379
|
+
title: `Plan Committed`,
|
|
380
|
+
message: `${targetBranch} — ${commitHash}`,
|
|
381
|
+
variant: "success",
|
|
382
|
+
duration: 5000,
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// Toast failure is non-fatal
|
|
388
|
+
}
|
|
389
|
+
return `✓ Plan committed to branch
|
|
390
|
+
|
|
391
|
+
Branch: ${targetBranch}${branchCreated ? " (created)" : ""}
|
|
392
|
+
Commit: ${commitHash} — ${commitMsg}
|
|
393
|
+
Plan: ${planFilename}
|
|
394
|
+
|
|
395
|
+
The plan and .cortex/ artifacts are committed on '${targetBranch}'.
|
|
396
|
+
Main branch is clean.
|
|
397
|
+
|
|
398
|
+
Next steps:
|
|
399
|
+
• Switch to Implement agent to begin coding
|
|
400
|
+
• Or use worktree_create to work in an isolated copy`;
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
}
|
|
176
404
|
// Export with underscore suffix to avoid reserved word
|
|
177
405
|
export { delete_ as delete };
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
export declare const qualityGateSummary: {
|
|
8
|
+
description: string;
|
|
9
|
+
args: {
|
|
10
|
+
scope: import("zod").ZodOptional<import("zod").ZodString>;
|
|
11
|
+
testing: import("zod").ZodOptional<import("zod").ZodString>;
|
|
12
|
+
security: import("zod").ZodOptional<import("zod").ZodString>;
|
|
13
|
+
audit: import("zod").ZodOptional<import("zod").ZodString>;
|
|
14
|
+
perf: import("zod").ZodOptional<import("zod").ZodString>;
|
|
15
|
+
devops: import("zod").ZodOptional<import("zod").ZodString>;
|
|
16
|
+
docsWriter: import("zod").ZodOptional<import("zod").ZodString>;
|
|
17
|
+
};
|
|
18
|
+
execute(args: {
|
|
19
|
+
scope?: string | undefined;
|
|
20
|
+
testing?: string | undefined;
|
|
21
|
+
security?: string | undefined;
|
|
22
|
+
audit?: string | undefined;
|
|
23
|
+
perf?: string | undefined;
|
|
24
|
+
devops?: string | undefined;
|
|
25
|
+
docsWriter?: string | undefined;
|
|
26
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=quality-gate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quality-gate.d.ts","sourceRoot":"","sources":["../../src/tools/quality-gate.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA6CH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;CA+I7B,CAAC"}
|