cortex-agents 3.4.0 → 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 (48) hide show
  1. package/.opencode/agents/architect.md +81 -89
  2. package/.opencode/agents/audit.md +57 -188
  3. package/.opencode/agents/{crosslayer.md → coder.md} +8 -52
  4. package/.opencode/agents/debug.md +151 -0
  5. package/.opencode/agents/devops.md +142 -0
  6. package/.opencode/agents/docs-writer.md +195 -0
  7. package/.opencode/agents/fix.md +118 -189
  8. package/.opencode/agents/implement.md +114 -74
  9. package/.opencode/agents/perf.md +151 -0
  10. package/.opencode/agents/refactor.md +163 -0
  11. package/.opencode/agents/{guard.md → security.md} +20 -85
  12. package/.opencode/agents/testing.md +115 -0
  13. package/.opencode/skills/data-engineering/SKILL.md +221 -0
  14. package/.opencode/skills/monitoring-observability/SKILL.md +251 -0
  15. package/README.md +302 -287
  16. package/dist/cli.js +6 -9
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +26 -28
  19. package/dist/registry.d.ts +4 -4
  20. package/dist/registry.d.ts.map +1 -1
  21. package/dist/registry.js +6 -6
  22. package/dist/tools/branch.d.ts +2 -2
  23. package/dist/tools/docs.d.ts +2 -2
  24. package/dist/tools/github.d.ts +3 -3
  25. package/dist/tools/plan.d.ts +28 -4
  26. package/dist/tools/plan.d.ts.map +1 -1
  27. package/dist/tools/plan.js +232 -4
  28. package/dist/tools/quality-gate.d.ts +28 -0
  29. package/dist/tools/quality-gate.d.ts.map +1 -0
  30. package/dist/tools/quality-gate.js +233 -0
  31. package/dist/tools/repl.d.ts +5 -0
  32. package/dist/tools/repl.d.ts.map +1 -1
  33. package/dist/tools/repl.js +58 -7
  34. package/dist/tools/worktree.d.ts +5 -32
  35. package/dist/tools/worktree.d.ts.map +1 -1
  36. package/dist/tools/worktree.js +75 -458
  37. package/dist/utils/change-scope.d.ts +33 -0
  38. package/dist/utils/change-scope.d.ts.map +1 -0
  39. package/dist/utils/change-scope.js +198 -0
  40. package/dist/utils/plan-extract.d.ts +21 -0
  41. package/dist/utils/plan-extract.d.ts.map +1 -1
  42. package/dist/utils/plan-extract.js +65 -0
  43. package/dist/utils/repl.d.ts +31 -0
  44. package/dist/utils/repl.d.ts.map +1 -1
  45. package/dist/utils/repl.js +126 -13
  46. package/package.json +1 -1
  47. package/.opencode/agents/qa.md +0 -265
  48. package/.opencode/agents/ship.md +0 -249
@@ -0,0 +1 @@
1
+ {"version":3,"file":"change-scope.d.ts","sourceRoot":"","sources":["../../src/utils/change-scope.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,KAAK,GAAG,UAAU,GAAG,MAAM,CAAC;AAElE,MAAM,WAAW,iBAAiB;IAChC,kCAAkC;IAClC,KAAK,EAAE,WAAW,CAAC;IACnB,sDAAsD;IACtD,SAAS,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,EAAE,OAAO,CAAC;CACrB;AAwHD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,iBAAiB,CAsE7E"}
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Change Scope Detection
3
+ *
4
+ * Categorizes changed files by risk level to determine which sub-agents
5
+ * should be triggered during the quality gate. Avoids wasting tokens on
6
+ * trivial changes while ensuring high-risk changes get full coverage.
7
+ */
8
+ // ─── File Pattern Matchers ───────────────────────────────────────────────────
9
+ /** Patterns that indicate trivial changes — docs, comments, formatting only */
10
+ const TRIVIAL_PATTERNS = [
11
+ /\.md$/i,
12
+ /\.txt$/i,
13
+ /\.mdx$/i,
14
+ /LICENSE/i,
15
+ /CHANGELOG/i,
16
+ /\.prettierrc/,
17
+ /\.editorconfig/,
18
+ /\.vscode\//,
19
+ /\.idea\//,
20
+ ];
21
+ /** Patterns that indicate test/config-only changes — low risk */
22
+ const LOW_RISK_PATTERNS = [
23
+ /\.test\.[jt]sx?$/,
24
+ /\.spec\.[jt]sx?$/,
25
+ /__tests__\//,
26
+ /test\//,
27
+ /tests\//,
28
+ /\.eslintrc/,
29
+ /\.prettierrc/,
30
+ /tsconfig.*\.json$/,
31
+ /jest\.config/,
32
+ /vitest\.config/,
33
+ /\.gitignore$/,
34
+ ];
35
+ /** Patterns that indicate high-risk changes — auth, payments, infra, security */
36
+ const HIGH_RISK_PATTERNS = [
37
+ // Auth & security
38
+ /auth/i,
39
+ /login/i,
40
+ /session/i,
41
+ /password/i,
42
+ /token/i,
43
+ /crypto/i,
44
+ /encrypt/i,
45
+ /permission/i,
46
+ /rbac/i,
47
+ /oauth/i,
48
+ /jwt/i,
49
+ /middleware\/auth/i,
50
+ // Payment & sensitive data
51
+ /payment/i,
52
+ /billing/i,
53
+ /stripe/i,
54
+ /checkout/i,
55
+ // Infrastructure & deployment
56
+ /Dockerfile/i,
57
+ /docker-compose/i,
58
+ /\.github\/workflows\//,
59
+ /\.gitlab-ci/,
60
+ /Jenkinsfile/i,
61
+ /\.circleci\//,
62
+ /terraform\//,
63
+ /pulumi\//,
64
+ /k8s\//,
65
+ /deploy\//,
66
+ /infra\//,
67
+ /nginx\.conf/i,
68
+ /Caddyfile/i,
69
+ /Procfile/i,
70
+ /fly\.toml/i,
71
+ ];
72
+ /** Patterns that indicate DevOps file changes */
73
+ const DEVOPS_PATTERNS = [
74
+ /Dockerfile/i,
75
+ /docker-compose/i,
76
+ /\.dockerignore/i,
77
+ /\.github\/workflows\//,
78
+ /\.gitlab-ci/,
79
+ /Jenkinsfile/i,
80
+ /\.circleci\//,
81
+ /terraform\//,
82
+ /pulumi\//,
83
+ /cdk\//,
84
+ /k8s\//,
85
+ /deploy\//,
86
+ /infra\//,
87
+ /nginx\.conf/i,
88
+ /Caddyfile/i,
89
+ /Procfile/i,
90
+ /fly\.toml/i,
91
+ /railway\.json/i,
92
+ /render\.yaml/i,
93
+ ];
94
+ /** Patterns that indicate performance-sensitive changes */
95
+ const PERF_PATTERNS = [
96
+ /query/i,
97
+ /database/i,
98
+ /migration/i,
99
+ /\.sql$/i,
100
+ /prisma/i,
101
+ /drizzle/i,
102
+ /repository/i,
103
+ /cache/i,
104
+ /render/i,
105
+ /component/i,
106
+ /hook/i,
107
+ /algorithm/i,
108
+ /sort/i,
109
+ /search/i,
110
+ /index/i,
111
+ /worker/i,
112
+ /stream/i,
113
+ /batch/i,
114
+ /queue/i,
115
+ ];
116
+ // ─── Classification ──────────────────────────────────────────────────────────
117
+ /**
118
+ * Classify a set of changed files into a risk scope and determine
119
+ * which sub-agents should be triggered.
120
+ *
121
+ * @param changedFiles - Array of file paths that were created or modified
122
+ * @returns Classification result with scope, rationale, and agent triggers
123
+ */
124
+ export function classifyChangeScope(changedFiles) {
125
+ if (changedFiles.length === 0) {
126
+ return {
127
+ scope: "trivial",
128
+ rationale: "No files changed",
129
+ agents: noAgents(),
130
+ };
131
+ }
132
+ const hasHighRisk = changedFiles.some((f) => HIGH_RISK_PATTERNS.some((p) => p.test(f)));
133
+ const hasDevOps = changedFiles.some((f) => DEVOPS_PATTERNS.some((p) => p.test(f)));
134
+ const hasPerf = changedFiles.some((f) => PERF_PATTERNS.some((p) => p.test(f)));
135
+ const allTrivial = changedFiles.every((f) => TRIVIAL_PATTERNS.some((p) => p.test(f)));
136
+ const allLowRisk = changedFiles.every((f) => LOW_RISK_PATTERNS.some((p) => p.test(f)) || TRIVIAL_PATTERNS.some((p) => p.test(f)));
137
+ // Trivial — docs/comments only
138
+ if (allTrivial) {
139
+ return {
140
+ scope: "trivial",
141
+ rationale: "Documentation/formatting changes only — no quality gate needed",
142
+ agents: {
143
+ ...noAgents(),
144
+ docsWriter: true,
145
+ },
146
+ };
147
+ }
148
+ // Low risk — tests/config only
149
+ if (allLowRisk) {
150
+ return {
151
+ scope: "low",
152
+ rationale: "Test/config changes only — minimal quality gate",
153
+ agents: {
154
+ ...noAgents(),
155
+ testing: true,
156
+ },
157
+ };
158
+ }
159
+ // High risk — auth, payments, infra
160
+ if (hasHighRisk) {
161
+ return {
162
+ scope: "high",
163
+ rationale: "High-risk changes detected (auth/security/payments/infra) — full quality gate",
164
+ agents: {
165
+ testing: true,
166
+ security: true,
167
+ audit: true,
168
+ devops: hasDevOps,
169
+ perf: hasPerf,
170
+ docsWriter: true,
171
+ },
172
+ };
173
+ }
174
+ // Standard — everything else
175
+ return {
176
+ scope: "standard",
177
+ rationale: "Standard code changes — normal quality gate",
178
+ agents: {
179
+ testing: true,
180
+ security: true,
181
+ audit: true,
182
+ devops: hasDevOps,
183
+ perf: hasPerf,
184
+ docsWriter: true,
185
+ },
186
+ };
187
+ }
188
+ /** Helper: no agents triggered */
189
+ function noAgents() {
190
+ return {
191
+ testing: false,
192
+ security: false,
193
+ audit: false,
194
+ devops: false,
195
+ perf: false,
196
+ docsWriter: false,
197
+ };
198
+ }
@@ -1,3 +1,17 @@
1
+ /**
2
+ * Map plan types to git branch prefixes.
3
+ */
4
+ export declare const TYPE_TO_PREFIX: Record<string, string>;
5
+ /**
6
+ * Parse YAML frontmatter from plan content.
7
+ * Returns a map of key-value pairs, or null if no frontmatter found.
8
+ */
9
+ export declare function parseFrontmatter(content: string): Record<string, string> | null;
10
+ /**
11
+ * Update or insert a field in the plan's YAML frontmatter.
12
+ * Returns the updated file content.
13
+ */
14
+ export declare function upsertFrontmatterField(content: string, key: string, value: string): string;
1
15
  /**
2
16
  * Sections extracted from a plan for use in a PR body.
3
17
  */
@@ -13,6 +27,13 @@ export interface PlanSections {
13
27
  /** The raw plan filename */
14
28
  filename: string;
15
29
  }
30
+ /**
31
+ * Extract the branch name from plan frontmatter.
32
+ *
33
+ * Looks for `branch: feature/xyz` in YAML frontmatter.
34
+ * Returns the branch name string, or null if not found.
35
+ */
36
+ export declare function extractBranch(planContent: string): string | null;
16
37
  /**
17
38
  * Extract GitHub issue references from plan frontmatter.
18
39
  *
@@ -1 +1 @@
1
- {"version":3,"file":"plan-extract.d.ts","sourceRoot":"","sources":["../../src/utils/plan-extract.ts"],"names":[],"mappings":"AAMA;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,4BAA4B;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAY9D;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY,CAuCvF;AAmCD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,CAwBlE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,YAAY,CAAC,EAAE,MAAM,EACrB,UAAU,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA0C9C"}
1
+ {"version":3,"file":"plan-extract.d.ts","sourceRoot":"","sources":["../../src/utils/plan-extract.ts"],"names":[],"mappings":"AAMA;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAOjD,CAAC;AAEF;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAY/E;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAiB1F;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,4BAA4B;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAShE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAY9D;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY,CAuCvF;AAmCD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,CAwBlE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,YAAY,CAAC,EAAE,MAAM,EACrB,UAAU,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA0C9C"}
@@ -2,6 +2,71 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  const CORTEX_DIR = ".cortex";
4
4
  const PLANS_DIR = "plans";
5
+ /**
6
+ * Map plan types to git branch prefixes.
7
+ */
8
+ export const TYPE_TO_PREFIX = {
9
+ feature: "feature",
10
+ bugfix: "bugfix",
11
+ refactor: "refactor",
12
+ architecture: "refactor",
13
+ spike: "feature",
14
+ docs: "docs",
15
+ };
16
+ /**
17
+ * Parse YAML frontmatter from plan content.
18
+ * Returns a map of key-value pairs, or null if no frontmatter found.
19
+ */
20
+ export function parseFrontmatter(content) {
21
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
22
+ if (!match)
23
+ return null;
24
+ const fm = {};
25
+ for (const line of match[1].split("\n")) {
26
+ const kv = line.match(/^(\w+):\s*"?([^"\n]*)"?\s*$/);
27
+ if (kv) {
28
+ fm[kv[1]] = kv[2].trim();
29
+ }
30
+ }
31
+ return fm;
32
+ }
33
+ /**
34
+ * Update or insert a field in the plan's YAML frontmatter.
35
+ * Returns the updated file content.
36
+ */
37
+ export function upsertFrontmatterField(content, key, value) {
38
+ const fmMatch = content.match(/^(---\n)([\s\S]*?)(\n---)/);
39
+ if (!fmMatch)
40
+ return content;
41
+ const fmBody = fmMatch[2];
42
+ const fieldRegex = new RegExp(`^${key}:\\s*.*$`, "m");
43
+ let updatedFm;
44
+ if (fieldRegex.test(fmBody)) {
45
+ // Update existing field
46
+ updatedFm = fmBody.replace(fieldRegex, `${key}: ${value}`);
47
+ }
48
+ else {
49
+ // Insert before the closing ---
50
+ updatedFm = fmBody + `\n${key}: ${value}`;
51
+ }
52
+ return fmMatch[1] + updatedFm + fmMatch[3] + content.slice(fmMatch[0].length);
53
+ }
54
+ /**
55
+ * Extract the branch name from plan frontmatter.
56
+ *
57
+ * Looks for `branch: feature/xyz` in YAML frontmatter.
58
+ * Returns the branch name string, or null if not found.
59
+ */
60
+ export function extractBranch(planContent) {
61
+ const frontmatterMatch = planContent.match(/^---\n([\s\S]*?)\n---/);
62
+ if (!frontmatterMatch)
63
+ return null;
64
+ const branchMatch = frontmatterMatch[1].match(/^branch:\s*(.+)$/m);
65
+ if (!branchMatch)
66
+ return null;
67
+ const branch = branchMatch[1].trim();
68
+ return branch || null;
69
+ }
5
70
  /**
6
71
  * Extract GitHub issue references from plan frontmatter.
7
72
  *
@@ -21,12 +21,18 @@ export interface ReplTask {
21
21
  index: number;
22
22
  /** Task description from the plan */
23
23
  description: string;
24
+ /** Acceptance criteria extracted from `- AC:` lines under the task */
25
+ acceptanceCriteria: string[];
24
26
  /** Current status in the state machine */
25
27
  status: TaskStatus;
26
28
  /** Number of failed attempts (resets on pass) */
27
29
  retries: number;
28
30
  /** Full iteration history */
29
31
  iterations: TaskIteration[];
32
+ /** ISO timestamp when the task was started */
33
+ startedAt?: string;
34
+ /** ISO timestamp when the task was completed/failed/skipped */
35
+ completedAt?: string;
30
36
  }
31
37
  export interface ReplState {
32
38
  /** Source plan filename */
@@ -48,6 +54,10 @@ export interface ReplState {
48
54
  /** All tasks in the loop */
49
55
  tasks: ReplTask[];
50
56
  }
57
+ export interface CortexConfig {
58
+ /** Max retries per task before escalating to user */
59
+ maxRetries?: number;
60
+ }
51
61
  export interface CommandDetection {
52
62
  buildCommand: string | null;
53
63
  testCommand: string | null;
@@ -57,14 +67,25 @@ export interface CommandDetection {
57
67
  /** Whether auto-detection found anything */
58
68
  detected: boolean;
59
69
  }
70
+ export interface ParsedTask {
71
+ description: string;
72
+ acceptanceCriteria: string[];
73
+ }
60
74
  /**
61
75
  * Parse plan tasks from plan markdown content.
62
76
  *
63
77
  * Looks for unchecked checkbox items (`- [ ] ...`) in a `## Tasks` section.
64
78
  * Falls back to any unchecked checkboxes anywhere in the document.
65
79
  * Strips the `Task N:` prefix if present to get a clean description.
80
+ * Extracts `- AC:` lines immediately following each task as acceptance criteria.
66
81
  */
67
82
  export declare function parseTasksFromPlan(planContent: string): string[];
83
+ /**
84
+ * Parse plan tasks with their acceptance criteria.
85
+ *
86
+ * Returns structured tasks including `- AC:` lines found under each checkbox item.
87
+ */
88
+ export declare function parseTasksWithAC(planContent: string): ParsedTask[];
68
89
  /**
69
90
  * Auto-detect build, test, and lint commands from project configuration files.
70
91
  *
@@ -77,6 +98,11 @@ export declare function parseTasksFromPlan(planContent: string): string[];
77
98
  * 6. mix.exs (Elixir)
78
99
  */
79
100
  export declare function detectCommands(cwd: string): Promise<CommandDetection>;
101
+ /**
102
+ * Read cortex config from .cortex/config.json.
103
+ * Returns an empty config if the file doesn't exist or is malformed.
104
+ */
105
+ export declare function readCortexConfig(cwd: string): CortexConfig;
80
106
  /**
81
107
  * Read the current REPL state from .cortex/repl-state.json.
82
108
  * Returns null if no state file exists.
@@ -101,6 +127,11 @@ export declare function getCurrentTask(state: ReplState): ReplTask | null;
101
127
  * Check if the loop is complete (no pending or in_progress tasks).
102
128
  */
103
129
  export declare function isLoopComplete(state: ReplState): boolean;
130
+ /**
131
+ * Detect if a previous REPL loop was interrupted mid-task.
132
+ * Returns the incomplete state if found, null otherwise.
133
+ */
134
+ export declare function detectIncompleteState(cwd: string): ReplState | null;
104
135
  /**
105
136
  * Format the current loop status as a human-readable string.
106
137
  * Used by repl_status tool output.
@@ -1 +1 @@
1
- {"version":3,"file":"repl.d.ts","sourceRoot":"","sources":["../../src/utils/repl.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAaH,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;AAErF,MAAM,WAAW,aAAa;IAC5B,sCAAsC;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,gCAAgC;IAChC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IACjC,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,QAAQ;IACvB,wCAAwC;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,qCAAqC;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,0CAA0C;IAC1C,MAAM,EAAE,UAAU,CAAC;IACnB,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,6BAA6B;IAC7B,UAAU,EAAE,aAAa,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,SAAS;IACxB,2BAA2B;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mDAAmD;IACnD,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,kDAAkD;IAClD,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,4BAA4B;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,kEAAkE;IAClE,UAAU,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,gBAAgB,EAAE,MAAM,CAAC;IACzB,4BAA4B;IAC5B,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,iEAAiE;IACjE,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,QAAQ,EAAE,OAAO,CAAC;CACnB;AAID;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAsBhE;AA4BD;;;;;;;;;;GAUG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA6H3E;AAWD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAwB3D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,IAAI,CAuBlE;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,GAAG,QAAQ,GAAG,IAAI,CAE7D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,GAAG,QAAQ,GAAG,IAAI,CAEhE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAIxD;AAWD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAwEvD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CA+EtD"}
1
+ {"version":3,"file":"repl.d.ts","sourceRoot":"","sources":["../../src/utils/repl.ts"],"names":[],"mappings":"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"}
@@ -13,30 +13,55 @@ import * as path from "path";
13
13
  // ─── Constants ───────────────────────────────────────────────────────────────
14
14
  const CORTEX_DIR = ".cortex";
15
15
  const REPL_STATE_FILE = "repl-state.json";
16
- // ─── Task Parsing ────────────────────────────────────────────────────────────
17
16
  /**
18
17
  * Parse plan tasks from plan markdown content.
19
18
  *
20
19
  * Looks for unchecked checkbox items (`- [ ] ...`) in a `## Tasks` section.
21
20
  * Falls back to any unchecked checkboxes anywhere in the document.
22
21
  * Strips the `Task N:` prefix if present to get a clean description.
22
+ * Extracts `- AC:` lines immediately following each task as acceptance criteria.
23
23
  */
24
24
  export function parseTasksFromPlan(planContent) {
25
- // Try to find a ## Tasks section first
25
+ return parseTasksWithAC(planContent).map((t) => t.description);
26
+ }
27
+ /**
28
+ * Parse plan tasks with their acceptance criteria.
29
+ *
30
+ * Returns structured tasks including `- AC:` lines found under each checkbox item.
31
+ */
32
+ export function parseTasksWithAC(planContent) {
26
33
  const tasksSection = extractTasksSection(planContent);
27
34
  const source = tasksSection || planContent;
28
35
  const tasks = [];
29
36
  const lines = source.split("\n");
30
- for (const line of lines) {
31
- // Match unchecked checkbox items: - [ ] Description
32
- const match = line.match(/^[-*]\s*\[\s\]\s+(.+)$/);
37
+ for (let i = 0; i < lines.length; i++) {
38
+ const match = lines[i].match(/^[-*]\s*\[\s\]\s+(.+)$/);
33
39
  if (match) {
34
40
  let description = match[1].trim();
35
- // Strip "Task N:" prefix if present
36
41
  description = description.replace(/^Task\s+\d+\s*:\s*/i, "");
37
- if (description) {
38
- tasks.push(description);
42
+ if (!description)
43
+ continue;
44
+ // Collect AC lines immediately following this task
45
+ const acs = [];
46
+ for (let j = i + 1; j < lines.length; j++) {
47
+ const acMatch = lines[j].match(/^\s+[-*]\s*AC:\s*(.+)$/);
48
+ if (acMatch) {
49
+ acs.push(acMatch[1].trim());
50
+ }
51
+ else if (lines[j].match(/^[-*]\s*\[/)) {
52
+ // Next task checkbox — stop collecting ACs
53
+ break;
54
+ }
55
+ else if (lines[j].trim() === "") {
56
+ // Blank line — continue (may be spacing between AC lines)
57
+ continue;
58
+ }
59
+ else {
60
+ // Non-AC, non-blank, non-checkbox line — stop
61
+ break;
62
+ }
39
63
  }
64
+ tasks.push({ description, acceptanceCriteria: acs });
40
65
  }
41
66
  }
42
67
  return tasks;
@@ -199,6 +224,28 @@ export async function detectCommands(cwd) {
199
224
  }
200
225
  return result;
201
226
  }
227
+ // ─── Config Reading ─────────────────────────────────────────────────────────
228
+ const CONFIG_FILE = "config.json";
229
+ /**
230
+ * Read cortex config from .cortex/config.json.
231
+ * Returns an empty config if the file doesn't exist or is malformed.
232
+ */
233
+ export function readCortexConfig(cwd) {
234
+ const configPath = path.join(cwd, CORTEX_DIR, CONFIG_FILE);
235
+ if (!fs.existsSync(configPath))
236
+ return {};
237
+ try {
238
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
239
+ if (typeof raw !== "object" || raw === null)
240
+ return {};
241
+ return {
242
+ maxRetries: typeof raw.maxRetries === "number" ? raw.maxRetries : undefined,
243
+ };
244
+ }
245
+ catch {
246
+ return {};
247
+ }
248
+ }
202
249
  // ─── State Management ────────────────────────────────────────────────────────
203
250
  /**
204
251
  * Get the path to the REPL state file.
@@ -226,6 +273,12 @@ export function readReplState(cwd) {
226
273
  !Array.isArray(raw.tasks)) {
227
274
  return null;
228
275
  }
276
+ // Backward compatibility: ensure all tasks have acceptanceCriteria
277
+ for (const task of raw.tasks) {
278
+ if (!Array.isArray(task.acceptanceCriteria)) {
279
+ task.acceptanceCriteria = [];
280
+ }
281
+ }
229
282
  return raw;
230
283
  }
231
284
  catch {
@@ -281,6 +334,44 @@ export function isLoopComplete(state) {
281
334
  return state.tasks.every((t) => t.status === "passed" || t.status === "failed" || t.status === "skipped");
282
335
  }
283
336
  // ─── Formatting ──────────────────────────────────────────────────────────────
337
+ /**
338
+ * Detect if a previous REPL loop was interrupted mid-task.
339
+ * Returns the incomplete state if found, null otherwise.
340
+ */
341
+ export function detectIncompleteState(cwd) {
342
+ const state = readReplState(cwd);
343
+ if (!state)
344
+ return null;
345
+ // Loop is already complete
346
+ if (isLoopComplete(state))
347
+ return null;
348
+ // There's an in_progress task — session was interrupted
349
+ const current = getCurrentTask(state);
350
+ if (current)
351
+ return state;
352
+ // There are pending tasks but no in_progress — also incomplete
353
+ const next = getNextTask(state);
354
+ if (next)
355
+ return state;
356
+ return null;
357
+ }
358
+ /**
359
+ * Format task duration as a human-readable string.
360
+ */
361
+ function formatTaskDuration(task) {
362
+ if (!task.startedAt)
363
+ return null;
364
+ const start = new Date(task.startedAt);
365
+ const end = task.completedAt ? new Date(task.completedAt) : new Date();
366
+ const durationMs = end.getTime() - start.getTime();
367
+ if (durationMs < 1000)
368
+ return "< 1s";
369
+ if (durationMs < 60_000)
370
+ return `${Math.round(durationMs / 1000)}s`;
371
+ const mins = Math.floor(durationMs / 60_000);
372
+ const secs = Math.round((durationMs % 60_000) / 1000);
373
+ return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
374
+ }
284
375
  /** Visual progress bar using block characters. */
285
376
  function progressBar(done, total, width = 20) {
286
377
  if (total === 0)
@@ -313,11 +404,23 @@ export function formatProgress(state) {
313
404
  if (current.retries > 0) {
314
405
  lines.push(` Attempt: ${current.retries + 1}/${state.maxRetries}`);
315
406
  }
407
+ if (current.acceptanceCriteria.length > 0) {
408
+ lines.push(` Acceptance Criteria:`);
409
+ for (const ac of current.acceptanceCriteria) {
410
+ lines.push(` - ${ac}`);
411
+ }
412
+ }
316
413
  }
317
414
  else if (next) {
318
415
  lines.push("");
319
416
  lines.push(`Next Task (#${next.index + 1}):`);
320
417
  lines.push(` "${next.description}"`);
418
+ if (next.acceptanceCriteria.length > 0) {
419
+ lines.push(` Acceptance Criteria:`);
420
+ for (const ac of next.acceptanceCriteria) {
421
+ lines.push(` - ${ac}`);
422
+ }
423
+ }
321
424
  }
322
425
  else if (isLoopComplete(state)) {
323
426
  lines.push("");
@@ -339,18 +442,20 @@ export function formatProgress(state) {
339
442
  const iterInfo = task.iterations.length > 0
340
443
  ? ` (${task.iterations.length} iteration${task.iterations.length > 1 ? "s" : ""}${task.retries > 0 ? `, ${task.retries} retr${task.retries > 1 ? "ies" : "y"}` : ""})`
341
444
  : "";
445
+ const timeInfo = formatTaskDuration(task);
446
+ const timeSuffix = timeInfo ? ` [${timeInfo}]` : "";
342
447
  switch (task.status) {
343
448
  case "passed":
344
- lines.push(` \u2713 ${num} ${task.description}${iterInfo}`);
449
+ lines.push(` \u2713 ${num} ${task.description}${iterInfo}${timeSuffix}`);
345
450
  break;
346
451
  case "failed":
347
- lines.push(` \u2717 ${num} ${task.description}${iterInfo}`);
452
+ lines.push(` \u2717 ${num} ${task.description}${iterInfo}${timeSuffix}`);
348
453
  break;
349
454
  case "skipped":
350
- lines.push(` \u2298 ${num} ${task.description}`);
455
+ lines.push(` \u2298 ${num} ${task.description}${timeSuffix}`);
351
456
  break;
352
457
  case "in_progress":
353
- lines.push(` \u25B6 ${num} ${task.description}${iterInfo}`);
458
+ lines.push(` \u25B6 ${num} ${task.description}${iterInfo}${timeSuffix}`);
354
459
  break;
355
460
  case "pending":
356
461
  lines.push(` \u25CB ${num} ${task.description}`);
@@ -407,7 +512,15 @@ export function formatSummary(state) {
407
512
  lines.push(`| ${num} | ${desc} | ${statusIcon} | ${attempts} |`);
408
513
  }
409
514
  lines.push("");
410
- lines.push(`**Results: ${passed} passed, ${failed} failed, ${skipped} skipped** (${totalIterations} total iterations)`);
515
+ const totalACs = state.tasks.reduce((sum, t) => sum + t.acceptanceCriteria.length, 0);
516
+ const passedACs = state.tasks
517
+ .filter((t) => t.status === "passed")
518
+ .reduce((sum, t) => sum + t.acceptanceCriteria.length, 0);
519
+ let resultsLine = `**Results: ${passed} passed, ${failed} failed, ${skipped} skipped** (${totalIterations} total iterations)`;
520
+ if (totalACs > 0) {
521
+ resultsLine += ` | **ACs: ${passedACs}/${totalACs} satisfied**`;
522
+ }
523
+ lines.push(resultsLine);
411
524
  // Timing
412
525
  if (state.startedAt) {
413
526
  const start = new Date(state.startedAt);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortex-agents",
3
- "version": "3.4.0",
3
+ "version": "4.0.0",
4
4
  "description": "Supercharge OpenCode with structured workflows, intelligent agents, and automated development practices",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",