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.
- package/.opencode/agents/architect.md +81 -89
- package/.opencode/agents/audit.md +57 -188
- package/.opencode/agents/{crosslayer.md → coder.md} +8 -52
- package/.opencode/agents/debug.md +151 -0
- package/.opencode/agents/devops.md +142 -0
- package/.opencode/agents/docs-writer.md +195 -0
- package/.opencode/agents/fix.md +118 -189
- package/.opencode/agents/implement.md +114 -74
- package/.opencode/agents/perf.md +151 -0
- package/.opencode/agents/refactor.md +163 -0
- package/.opencode/agents/{guard.md → security.md} +20 -85
- package/.opencode/agents/testing.md +115 -0
- package/.opencode/skills/data-engineering/SKILL.md +221 -0
- package/.opencode/skills/monitoring-observability/SKILL.md +251 -0
- package/README.md +302 -287
- package/dist/cli.js +6 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -28
- package/dist/registry.d.ts +4 -4
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +6 -6
- package/dist/tools/branch.d.ts +2 -2
- package/dist/tools/docs.d.ts +2 -2
- package/dist/tools/github.d.ts +3 -3
- 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 +5 -0
- package/dist/tools/repl.d.ts.map +1 -1
- package/dist/tools/repl.js +58 -7
- package/dist/tools/worktree.d.ts +5 -32
- package/dist/tools/worktree.d.ts.map +1 -1
- package/dist/tools/worktree.js +75 -458
- 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/plan-extract.d.ts +21 -0
- package/dist/utils/plan-extract.d.ts.map +1 -1
- package/dist/utils/plan-extract.js +65 -0
- package/dist/utils/repl.d.ts +31 -0
- package/dist/utils/repl.d.ts.map +1 -1
- package/dist/utils/repl.js +126 -13
- package/package.json +1 -1
- package/.opencode/agents/qa.md +0 -265
- 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
|
*
|
package/dist/utils/repl.d.ts
CHANGED
|
@@ -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.
|
package/dist/utils/repl.d.ts.map
CHANGED
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"repl.d.ts","sourceRoot":"","sources":["../../src/utils/repl.ts"],"names":[],"mappings":"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"}
|
package/dist/utils/repl.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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