cortex-agents 2.3.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/.opencode/agents/{plan.md → architect.md} +104 -58
  2. package/.opencode/agents/audit.md +183 -0
  3. package/.opencode/agents/{fullstack.md → coder.md} +10 -54
  4. package/.opencode/agents/debug.md +76 -201
  5. package/.opencode/agents/devops.md +16 -123
  6. package/.opencode/agents/docs-writer.md +195 -0
  7. package/.opencode/agents/fix.md +207 -0
  8. package/.opencode/agents/implement.md +433 -0
  9. package/.opencode/agents/perf.md +151 -0
  10. package/.opencode/agents/refactor.md +163 -0
  11. package/.opencode/agents/security.md +20 -85
  12. package/.opencode/agents/testing.md +1 -151
  13. package/.opencode/skills/data-engineering/SKILL.md +221 -0
  14. package/.opencode/skills/monitoring-observability/SKILL.md +251 -0
  15. package/README.md +315 -224
  16. package/dist/cli.js +85 -17
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +60 -22
  19. package/dist/registry.d.ts +8 -3
  20. package/dist/registry.d.ts.map +1 -1
  21. package/dist/registry.js +16 -2
  22. package/dist/tools/branch.d.ts +2 -2
  23. package/dist/tools/cortex.d.ts +2 -2
  24. package/dist/tools/cortex.js +7 -7
  25. package/dist/tools/docs.d.ts +2 -2
  26. package/dist/tools/environment.d.ts +31 -0
  27. package/dist/tools/environment.d.ts.map +1 -0
  28. package/dist/tools/environment.js +93 -0
  29. package/dist/tools/github.d.ts +42 -0
  30. package/dist/tools/github.d.ts.map +1 -0
  31. package/dist/tools/github.js +200 -0
  32. package/dist/tools/plan.d.ts +28 -4
  33. package/dist/tools/plan.d.ts.map +1 -1
  34. package/dist/tools/plan.js +232 -4
  35. package/dist/tools/quality-gate.d.ts +28 -0
  36. package/dist/tools/quality-gate.d.ts.map +1 -0
  37. package/dist/tools/quality-gate.js +233 -0
  38. package/dist/tools/repl.d.ts +55 -0
  39. package/dist/tools/repl.d.ts.map +1 -0
  40. package/dist/tools/repl.js +291 -0
  41. package/dist/tools/task.d.ts +2 -0
  42. package/dist/tools/task.d.ts.map +1 -1
  43. package/dist/tools/task.js +25 -30
  44. package/dist/tools/worktree.d.ts +5 -32
  45. package/dist/tools/worktree.d.ts.map +1 -1
  46. package/dist/tools/worktree.js +75 -447
  47. package/dist/utils/change-scope.d.ts +33 -0
  48. package/dist/utils/change-scope.d.ts.map +1 -0
  49. package/dist/utils/change-scope.js +198 -0
  50. package/dist/utils/github.d.ts +104 -0
  51. package/dist/utils/github.d.ts.map +1 -0
  52. package/dist/utils/github.js +243 -0
  53. package/dist/utils/ide.d.ts +76 -0
  54. package/dist/utils/ide.d.ts.map +1 -0
  55. package/dist/utils/ide.js +307 -0
  56. package/dist/utils/plan-extract.d.ts +28 -0
  57. package/dist/utils/plan-extract.d.ts.map +1 -1
  58. package/dist/utils/plan-extract.js +90 -1
  59. package/dist/utils/repl.d.ts +145 -0
  60. package/dist/utils/repl.d.ts.map +1 -0
  61. package/dist/utils/repl.js +547 -0
  62. package/dist/utils/terminal.d.ts +53 -1
  63. package/dist/utils/terminal.d.ts.map +1 -1
  64. package/dist/utils/terminal.js +642 -5
  65. package/package.json +1 -1
  66. package/.opencode/agents/build.md +0 -294
  67. package/.opencode/agents/review.md +0 -314
  68. package/dist/plugin.d.ts +0 -1
  69. package/dist/plugin.d.ts.map +0 -1
  70. package/dist/plugin.js +0 -4
@@ -0,0 +1,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
+ });
@@ -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: "feature" | "bugfix" | "refactor" | "spike" | "docs" | "architecture";
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?: "feature" | "bugfix" | "refactor" | "spike" | "docs" | "architecture" | "all" | undefined;
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
@@ -1 +1 @@
1
- {"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../src/tools/plan.ts"],"names":[],"mappings":"AAgCA,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;CA2Df,CAAC;AAEH,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;CAkEf,CAAC;AAEH,eAAO,MAAM,IAAI;;;;;;;;CAuBf,CAAC;AAEH,eAAO,MAAM,OAAO;;;;;;;;CAkBlB,CAAC;AAGH,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,CAAC"}
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"}
@@ -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.join(plansPath, filename);
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.join(plansPath, filename);
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"}