sequant 1.18.0 → 1.20.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/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/dist/bin/cli.js +7 -0
- package/dist/src/commands/conventions.d.ts +9 -0
- package/dist/src/commands/conventions.js +61 -0
- package/dist/src/commands/init.js +12 -0
- package/dist/src/lib/conventions-detector.d.ts +62 -0
- package/dist/src/lib/conventions-detector.js +510 -0
- package/dist/src/lib/fs.d.ts +1 -1
- package/dist/src/lib/settings.d.ts +8 -0
- package/dist/src/lib/settings.js +1 -0
- package/dist/src/lib/stacks.d.ts +4 -2
- package/dist/src/lib/stacks.js +43 -3
- package/dist/src/lib/workflow/state-schema.d.ts +5 -5
- package/package.json +7 -7
- package/templates/skills/exec/SKILL.md +1086 -29
- package/templates/skills/qa/SKILL.md +1732 -156
- package/templates/skills/test/SKILL.md +246 -1
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codebase conventions detector
|
|
3
|
+
*
|
|
4
|
+
* Deterministic detection of observable codebase patterns.
|
|
5
|
+
* No AI/ML — just file scanning and pattern matching.
|
|
6
|
+
*/
|
|
7
|
+
import { readdir, stat } from "fs/promises";
|
|
8
|
+
import { join, extname } from "path";
|
|
9
|
+
import { fileExists, readFile, writeFile, ensureDir } from "./fs.js";
|
|
10
|
+
/** Path to conventions file */
|
|
11
|
+
export const CONVENTIONS_PATH = ".sequant/conventions.json";
|
|
12
|
+
/** Directories to skip during scanning */
|
|
13
|
+
const SKIP_DIRS = new Set([
|
|
14
|
+
"node_modules",
|
|
15
|
+
".git",
|
|
16
|
+
"dist",
|
|
17
|
+
"build",
|
|
18
|
+
".next",
|
|
19
|
+
".nuxt",
|
|
20
|
+
".output",
|
|
21
|
+
"__pycache__",
|
|
22
|
+
"target",
|
|
23
|
+
".claude",
|
|
24
|
+
".sequant",
|
|
25
|
+
"coverage",
|
|
26
|
+
".turbo",
|
|
27
|
+
".cache",
|
|
28
|
+
"vendor",
|
|
29
|
+
]);
|
|
30
|
+
/**
|
|
31
|
+
* Collect source files up to a limit, skipping irrelevant directories
|
|
32
|
+
*/
|
|
33
|
+
async function collectFiles(dir, extensions, maxFiles, depth = 0) {
|
|
34
|
+
if (depth > 5)
|
|
35
|
+
return [];
|
|
36
|
+
const results = [];
|
|
37
|
+
let entries;
|
|
38
|
+
try {
|
|
39
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return results;
|
|
43
|
+
}
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
if (results.length >= maxFiles)
|
|
46
|
+
break;
|
|
47
|
+
if (entry.isDirectory()) {
|
|
48
|
+
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith("."))
|
|
49
|
+
continue;
|
|
50
|
+
const sub = await collectFiles(join(dir, entry.name), extensions, maxFiles - results.length, depth + 1);
|
|
51
|
+
results.push(...sub);
|
|
52
|
+
}
|
|
53
|
+
else if (entry.isFile() && extensions.has(extname(entry.name))) {
|
|
54
|
+
results.push(join(dir, entry.name));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Count occurrences of a pattern in file contents
|
|
61
|
+
*/
|
|
62
|
+
async function countPattern(files, pattern, maxFiles = 50) {
|
|
63
|
+
let count = 0;
|
|
64
|
+
for (const file of files.slice(0, maxFiles)) {
|
|
65
|
+
try {
|
|
66
|
+
const content = await readFile(file);
|
|
67
|
+
const matches = content.match(pattern);
|
|
68
|
+
if (matches)
|
|
69
|
+
count += matches.length;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Skip unreadable files
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return count;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Detect test file naming convention
|
|
79
|
+
*/
|
|
80
|
+
async function detectTestPattern(root) {
|
|
81
|
+
const testFiles = await collectFiles(root, new Set([".ts", ".tsx", ".js", ".jsx"]), 500);
|
|
82
|
+
const dotTest = testFiles.filter((f) => /\.test\.[jt]sx?$/.test(f));
|
|
83
|
+
const dotSpec = testFiles.filter((f) => /\.spec\.[jt]sx?$/.test(f));
|
|
84
|
+
const underscoreTests = testFiles.filter((f) => f.includes("__tests__/"));
|
|
85
|
+
if (dotTest.length === 0 &&
|
|
86
|
+
dotSpec.length === 0 &&
|
|
87
|
+
underscoreTests.length === 0) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
let value;
|
|
91
|
+
let evidence;
|
|
92
|
+
if (dotTest.length >= dotSpec.length &&
|
|
93
|
+
dotTest.length >= underscoreTests.length) {
|
|
94
|
+
value = "*.test.ts";
|
|
95
|
+
evidence = `${dotTest.length} .test.* files found`;
|
|
96
|
+
}
|
|
97
|
+
else if (dotSpec.length >= underscoreTests.length) {
|
|
98
|
+
value = "*.spec.ts";
|
|
99
|
+
evidence = `${dotSpec.length} .spec.* files found`;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
value = "__tests__/";
|
|
103
|
+
evidence = `${underscoreTests.length} files in __tests__/ directories`;
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
key: "testFilePattern",
|
|
107
|
+
label: "Test file pattern",
|
|
108
|
+
value,
|
|
109
|
+
source: "detected",
|
|
110
|
+
evidence,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Detect export style preference (named vs default)
|
|
115
|
+
*/
|
|
116
|
+
async function detectExportStyle(root) {
|
|
117
|
+
const srcDir = join(root, "src");
|
|
118
|
+
const searchDir = (await fileExists(srcDir)) ? srcDir : root;
|
|
119
|
+
const files = await collectFiles(searchDir, new Set([".ts", ".tsx", ".js", ".jsx"]), 100);
|
|
120
|
+
if (files.length === 0)
|
|
121
|
+
return null;
|
|
122
|
+
const defaultExports = await countPattern(files, /export\s+default\b/g, 50);
|
|
123
|
+
const namedExports = await countPattern(files, /export\s+(?:async\s+)?(?:function|class|const|let|interface|type|enum)\b/g, 50);
|
|
124
|
+
if (defaultExports === 0 && namedExports === 0)
|
|
125
|
+
return null;
|
|
126
|
+
const total = defaultExports + namedExports;
|
|
127
|
+
const namedRatio = namedExports / total;
|
|
128
|
+
let value;
|
|
129
|
+
if (namedRatio > 0.7) {
|
|
130
|
+
value = "named";
|
|
131
|
+
}
|
|
132
|
+
else if (namedRatio < 0.3) {
|
|
133
|
+
value = "default";
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
value = "mixed";
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
key: "exportStyle",
|
|
140
|
+
label: "Export style",
|
|
141
|
+
value,
|
|
142
|
+
source: "detected",
|
|
143
|
+
evidence: `${namedExports} named, ${defaultExports} default exports`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Detect async pattern preference
|
|
148
|
+
*/
|
|
149
|
+
async function detectAsyncPattern(root) {
|
|
150
|
+
const srcDir = join(root, "src");
|
|
151
|
+
const searchDir = (await fileExists(srcDir)) ? srcDir : root;
|
|
152
|
+
const files = await collectFiles(searchDir, new Set([".ts", ".tsx", ".js", ".jsx"]), 100);
|
|
153
|
+
if (files.length === 0)
|
|
154
|
+
return null;
|
|
155
|
+
const awaitCount = await countPattern(files, /\bawait\b/g, 50);
|
|
156
|
+
const thenCount = await countPattern(files, /\.then\s*\(/g, 50);
|
|
157
|
+
if (awaitCount === 0 && thenCount === 0)
|
|
158
|
+
return null;
|
|
159
|
+
const total = awaitCount + thenCount;
|
|
160
|
+
const awaitRatio = awaitCount / total;
|
|
161
|
+
let value;
|
|
162
|
+
if (awaitRatio > 0.7) {
|
|
163
|
+
value = "async/await";
|
|
164
|
+
}
|
|
165
|
+
else if (awaitRatio < 0.3) {
|
|
166
|
+
value = "promise-chains";
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
value = "mixed";
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
key: "asyncPattern",
|
|
173
|
+
label: "Async pattern",
|
|
174
|
+
value,
|
|
175
|
+
source: "detected",
|
|
176
|
+
evidence: `${awaitCount} await, ${thenCount} .then() usages`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Detect TypeScript strictness
|
|
181
|
+
*/
|
|
182
|
+
async function detectTypeScriptConfig(root) {
|
|
183
|
+
const tsConfigPath = join(root, "tsconfig.json");
|
|
184
|
+
if (!(await fileExists(tsConfigPath)))
|
|
185
|
+
return null;
|
|
186
|
+
try {
|
|
187
|
+
const content = await readFile(tsConfigPath);
|
|
188
|
+
// Strip comments (single-line) for JSON parsing
|
|
189
|
+
const stripped = content.replace(/\/\/.*$/gm, "");
|
|
190
|
+
const config = JSON.parse(stripped);
|
|
191
|
+
const strict = config?.compilerOptions?.strict;
|
|
192
|
+
return {
|
|
193
|
+
key: "typescriptStrict",
|
|
194
|
+
label: "TypeScript strict mode",
|
|
195
|
+
value: strict ? "enabled" : "disabled",
|
|
196
|
+
source: "detected",
|
|
197
|
+
evidence: `tsconfig.json compilerOptions.strict = ${strict}`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Detect source directory structure
|
|
206
|
+
*/
|
|
207
|
+
async function detectSourceStructure(root) {
|
|
208
|
+
const candidates = [
|
|
209
|
+
{ path: "src", label: "src/" },
|
|
210
|
+
{ path: "lib", label: "lib/" },
|
|
211
|
+
{ path: "app", label: "app/" },
|
|
212
|
+
{ path: "pages", label: "pages/" },
|
|
213
|
+
];
|
|
214
|
+
const found = [];
|
|
215
|
+
for (const c of candidates) {
|
|
216
|
+
const fullPath = join(root, c.path);
|
|
217
|
+
try {
|
|
218
|
+
const s = await stat(fullPath);
|
|
219
|
+
if (s.isDirectory())
|
|
220
|
+
found.push(c.label);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// doesn't exist
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (found.length === 0)
|
|
227
|
+
return null;
|
|
228
|
+
return {
|
|
229
|
+
key: "sourceStructure",
|
|
230
|
+
label: "Source directory structure",
|
|
231
|
+
value: found.join(", "),
|
|
232
|
+
source: "detected",
|
|
233
|
+
evidence: `Found directories: ${found.join(", ")}`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Detect package manager from lockfiles
|
|
238
|
+
*/
|
|
239
|
+
async function detectPackageManagerConvention(root) {
|
|
240
|
+
const lockfiles = [
|
|
241
|
+
{ file: "bun.lockb", manager: "bun" },
|
|
242
|
+
{ file: "bun.lock", manager: "bun" },
|
|
243
|
+
{ file: "pnpm-lock.yaml", manager: "pnpm" },
|
|
244
|
+
{ file: "yarn.lock", manager: "yarn" },
|
|
245
|
+
{ file: "package-lock.json", manager: "npm" },
|
|
246
|
+
];
|
|
247
|
+
for (const { file, manager } of lockfiles) {
|
|
248
|
+
if (await fileExists(join(root, file))) {
|
|
249
|
+
return {
|
|
250
|
+
key: "packageManager",
|
|
251
|
+
label: "Package manager",
|
|
252
|
+
value: manager,
|
|
253
|
+
source: "detected",
|
|
254
|
+
evidence: `Found ${file}`,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Detect indentation style from source files
|
|
262
|
+
*/
|
|
263
|
+
async function detectIndentation(root) {
|
|
264
|
+
const srcDir = join(root, "src");
|
|
265
|
+
const searchDir = (await fileExists(srcDir)) ? srcDir : root;
|
|
266
|
+
const files = await collectFiles(searchDir, new Set([".ts", ".tsx", ".js", ".jsx"]), 30);
|
|
267
|
+
if (files.length === 0)
|
|
268
|
+
return null;
|
|
269
|
+
let twoSpace = 0;
|
|
270
|
+
let fourSpace = 0;
|
|
271
|
+
let tabs = 0;
|
|
272
|
+
for (const file of files.slice(0, 20)) {
|
|
273
|
+
try {
|
|
274
|
+
const content = await readFile(file);
|
|
275
|
+
const lines = content.split("\n").slice(0, 50);
|
|
276
|
+
for (const line of lines) {
|
|
277
|
+
if (/^\t/.test(line))
|
|
278
|
+
tabs++;
|
|
279
|
+
else if (/^ {2}[^ ]/.test(line))
|
|
280
|
+
twoSpace++;
|
|
281
|
+
else if (/^ {4}[^ ]/.test(line))
|
|
282
|
+
fourSpace++;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
// skip
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const total = twoSpace + fourSpace + tabs;
|
|
290
|
+
if (total === 0)
|
|
291
|
+
return null;
|
|
292
|
+
let value;
|
|
293
|
+
if (tabs > twoSpace && tabs > fourSpace) {
|
|
294
|
+
value = "tabs";
|
|
295
|
+
}
|
|
296
|
+
else if (twoSpace >= fourSpace) {
|
|
297
|
+
value = "2 spaces";
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
value = "4 spaces";
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
key: "indentation",
|
|
304
|
+
label: "Indentation",
|
|
305
|
+
value,
|
|
306
|
+
source: "detected",
|
|
307
|
+
evidence: `${twoSpace} two-space, ${fourSpace} four-space, ${tabs} tab-indented lines`,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Detect semicolon usage
|
|
312
|
+
*/
|
|
313
|
+
async function detectSemicolons(root) {
|
|
314
|
+
const srcDir = join(root, "src");
|
|
315
|
+
const searchDir = (await fileExists(srcDir)) ? srcDir : root;
|
|
316
|
+
const files = await collectFiles(searchDir, new Set([".ts", ".tsx", ".js", ".jsx"]), 30);
|
|
317
|
+
if (files.length === 0)
|
|
318
|
+
return null;
|
|
319
|
+
let withSemicolon = 0;
|
|
320
|
+
let withoutSemicolon = 0;
|
|
321
|
+
for (const file of files.slice(0, 20)) {
|
|
322
|
+
try {
|
|
323
|
+
const content = await readFile(file);
|
|
324
|
+
const lines = content.split("\n");
|
|
325
|
+
for (const line of lines) {
|
|
326
|
+
const trimmed = line.trim();
|
|
327
|
+
// Skip empty lines, comments, opening/closing brackets
|
|
328
|
+
if (!trimmed ||
|
|
329
|
+
trimmed.startsWith("//") ||
|
|
330
|
+
trimmed.startsWith("/*") ||
|
|
331
|
+
trimmed.startsWith("*") ||
|
|
332
|
+
/^[{}()[\]]$/.test(trimmed) ||
|
|
333
|
+
/^import\s/.test(trimmed) ||
|
|
334
|
+
/^export\s/.test(trimmed))
|
|
335
|
+
continue;
|
|
336
|
+
if (trimmed.endsWith(";"))
|
|
337
|
+
withSemicolon++;
|
|
338
|
+
else if (trimmed.endsWith(")") ||
|
|
339
|
+
trimmed.endsWith('"') ||
|
|
340
|
+
trimmed.endsWith("'") ||
|
|
341
|
+
trimmed.endsWith("`") ||
|
|
342
|
+
/\w$/.test(trimmed))
|
|
343
|
+
withoutSemicolon++;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
// skip
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const total = withSemicolon + withoutSemicolon;
|
|
351
|
+
if (total === 0)
|
|
352
|
+
return null;
|
|
353
|
+
const semiRatio = withSemicolon / total;
|
|
354
|
+
const value = semiRatio > 0.6 ? "required" : semiRatio < 0.3 ? "omitted" : "mixed";
|
|
355
|
+
return {
|
|
356
|
+
key: "semicolons",
|
|
357
|
+
label: "Semicolons",
|
|
358
|
+
value,
|
|
359
|
+
source: "detected",
|
|
360
|
+
evidence: `${withSemicolon} with, ${withoutSemicolon} without semicolons`,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Detect component directory structure (for frontend projects)
|
|
365
|
+
*/
|
|
366
|
+
async function detectComponentStructure(root) {
|
|
367
|
+
const candidates = [
|
|
368
|
+
"src/components",
|
|
369
|
+
"components",
|
|
370
|
+
"src/app",
|
|
371
|
+
"app",
|
|
372
|
+
"src/pages",
|
|
373
|
+
"pages",
|
|
374
|
+
];
|
|
375
|
+
for (const candidate of candidates) {
|
|
376
|
+
const dirPath = join(root, candidate);
|
|
377
|
+
try {
|
|
378
|
+
const s = await stat(dirPath);
|
|
379
|
+
if (s.isDirectory()) {
|
|
380
|
+
return {
|
|
381
|
+
key: "componentDir",
|
|
382
|
+
label: "Component directory",
|
|
383
|
+
value: candidate + "/",
|
|
384
|
+
source: "detected",
|
|
385
|
+
evidence: `Directory exists: ${candidate}/`,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
// doesn't exist
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Run all convention detectors
|
|
397
|
+
*/
|
|
398
|
+
export async function detectConventions(projectRoot) {
|
|
399
|
+
const detectors = [
|
|
400
|
+
detectTestPattern,
|
|
401
|
+
detectExportStyle,
|
|
402
|
+
detectAsyncPattern,
|
|
403
|
+
detectTypeScriptConfig,
|
|
404
|
+
detectSourceStructure,
|
|
405
|
+
detectPackageManagerConvention,
|
|
406
|
+
detectIndentation,
|
|
407
|
+
detectSemicolons,
|
|
408
|
+
detectComponentStructure,
|
|
409
|
+
];
|
|
410
|
+
const results = [];
|
|
411
|
+
for (const detector of detectors) {
|
|
412
|
+
try {
|
|
413
|
+
const result = await detector(projectRoot);
|
|
414
|
+
if (result)
|
|
415
|
+
results.push(result);
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
// Skip failed detectors
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return results;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Load existing conventions file
|
|
425
|
+
*/
|
|
426
|
+
export async function loadConventions() {
|
|
427
|
+
if (!(await fileExists(CONVENTIONS_PATH)))
|
|
428
|
+
return null;
|
|
429
|
+
try {
|
|
430
|
+
const content = await readFile(CONVENTIONS_PATH);
|
|
431
|
+
return JSON.parse(content);
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Save conventions, preserving manual entries
|
|
439
|
+
*/
|
|
440
|
+
export async function saveConventions(detected) {
|
|
441
|
+
// Load existing to preserve manual entries
|
|
442
|
+
const existing = await loadConventions();
|
|
443
|
+
const manual = existing?.manual ?? {};
|
|
444
|
+
const detectedMap = {};
|
|
445
|
+
for (const c of detected) {
|
|
446
|
+
detectedMap[c.key] = c.value;
|
|
447
|
+
}
|
|
448
|
+
const conventions = {
|
|
449
|
+
detected: detectedMap,
|
|
450
|
+
manual,
|
|
451
|
+
detectedAt: new Date().toISOString(),
|
|
452
|
+
};
|
|
453
|
+
await ensureDir(".sequant");
|
|
454
|
+
await writeFile(CONVENTIONS_PATH, JSON.stringify(conventions, null, 2));
|
|
455
|
+
return conventions;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Get merged conventions (manual overrides detected)
|
|
459
|
+
*/
|
|
460
|
+
export function getMergedConventions(file) {
|
|
461
|
+
return { ...file.detected, ...file.manual };
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Format conventions for display
|
|
465
|
+
*/
|
|
466
|
+
export function formatConventions(file) {
|
|
467
|
+
const lines = [];
|
|
468
|
+
lines.push("Detected conventions:");
|
|
469
|
+
const detected = Object.entries(file.detected);
|
|
470
|
+
if (detected.length === 0) {
|
|
471
|
+
lines.push(" (none)");
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
for (const [key, value] of detected) {
|
|
475
|
+
lines.push(` ${key}: ${value}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const manual = Object.entries(file.manual);
|
|
479
|
+
if (manual.length > 0) {
|
|
480
|
+
lines.push("");
|
|
481
|
+
lines.push("Manual overrides:");
|
|
482
|
+
for (const [key, value] of manual) {
|
|
483
|
+
lines.push(` ${key}: ${value}`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
lines.push("");
|
|
487
|
+
lines.push(`Last detected: ${file.detectedAt}`);
|
|
488
|
+
return lines.join("\n");
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Detect and save conventions in one call
|
|
492
|
+
*/
|
|
493
|
+
export async function detectAndSaveConventions(projectRoot) {
|
|
494
|
+
const conventions = await detectConventions(projectRoot);
|
|
495
|
+
return saveConventions(conventions);
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Format conventions as context for AI skills
|
|
499
|
+
*/
|
|
500
|
+
export function formatConventionsForContext(file) {
|
|
501
|
+
const merged = getMergedConventions(file);
|
|
502
|
+
const entries = Object.entries(merged);
|
|
503
|
+
if (entries.length === 0)
|
|
504
|
+
return "";
|
|
505
|
+
const lines = ["## Codebase Conventions", ""];
|
|
506
|
+
for (const [key, value] of entries) {
|
|
507
|
+
lines.push(`- **${key}**: ${value}`);
|
|
508
|
+
}
|
|
509
|
+
return lines.join("\n");
|
|
510
|
+
}
|
package/dist/src/lib/fs.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export declare function isExecutable(path: string): Promise<boolean>;
|
|
|
6
6
|
export declare function ensureDir(path: string): Promise<void>;
|
|
7
7
|
export declare function readFile(path: string): Promise<string>;
|
|
8
8
|
export declare function writeFile(path: string, content: string): Promise<void>;
|
|
9
|
-
export declare function getFileStats(path: string): Promise<import("fs").Stats>;
|
|
9
|
+
export declare function getFileStats(path: string): Promise<import("node:fs").Stats>;
|
|
10
10
|
/**
|
|
11
11
|
* Check if a path is a symbolic link
|
|
12
12
|
*/
|
|
@@ -87,6 +87,14 @@ export interface RunSettings {
|
|
|
87
87
|
* Default: true
|
|
88
88
|
*/
|
|
89
89
|
retry: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Threshold for stale branch detection in pre-flight checks.
|
|
92
|
+
* If feature branch is more than this many commits behind main,
|
|
93
|
+
* QA/test skills block execution and recommend rebase.
|
|
94
|
+
* exec skill warns but doesn't block.
|
|
95
|
+
* Default: 5
|
|
96
|
+
*/
|
|
97
|
+
staleBranchThreshold: number;
|
|
90
98
|
}
|
|
91
99
|
/**
|
|
92
100
|
* Scope assessment threshold configuration
|
package/dist/src/lib/settings.js
CHANGED
|
@@ -85,6 +85,7 @@ export const DEFAULT_SETTINGS = {
|
|
|
85
85
|
rotation: DEFAULT_ROTATION_SETTINGS,
|
|
86
86
|
mcp: true, // Enable MCP servers by default in headless mode
|
|
87
87
|
retry: true, // Enable automatic retry with MCP fallback by default
|
|
88
|
+
staleBranchThreshold: 5, // Block QA/test if feature is >5 commits behind main
|
|
88
89
|
},
|
|
89
90
|
agents: DEFAULT_AGENT_SETTINGS,
|
|
90
91
|
scopeAssessment: DEFAULT_SCOPE_ASSESSMENT_SETTINGS,
|
package/dist/src/lib/stacks.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export interface StackConfig_Persisted {
|
|
|
28
28
|
/**
|
|
29
29
|
* Supported package managers
|
|
30
30
|
*/
|
|
31
|
-
export type PackageManager = "npm" | "bun" | "yarn" | "pnpm";
|
|
31
|
+
export type PackageManager = "npm" | "bun" | "yarn" | "pnpm" | "pip" | "poetry" | "uv";
|
|
32
32
|
/**
|
|
33
33
|
* Package manager command configuration
|
|
34
34
|
*/
|
|
@@ -44,8 +44,10 @@ export interface PackageManagerConfig {
|
|
|
44
44
|
export declare const PM_CONFIG: Record<PackageManager, PackageManagerConfig>;
|
|
45
45
|
/**
|
|
46
46
|
* Detect package manager from lockfiles
|
|
47
|
-
* Priority: bun > yarn > pnpm > npm
|
|
47
|
+
* Priority: bun > yarn > pnpm > npm (for JS)
|
|
48
|
+
* Priority: uv > poetry > pip (for Python)
|
|
48
49
|
* Falls back to npm if no lockfile found but package.json exists
|
|
50
|
+
* Falls back to pip if no lockfile found but pyproject.toml/requirements.txt exists
|
|
49
51
|
*/
|
|
50
52
|
export declare function detectPackageManager(): Promise<PackageManager | null>;
|
|
51
53
|
/**
|
package/dist/src/lib/stacks.js
CHANGED
|
@@ -48,6 +48,25 @@ export const PM_CONFIG = {
|
|
|
48
48
|
install: "pnpm install",
|
|
49
49
|
installSilent: "pnpm install --silent",
|
|
50
50
|
},
|
|
51
|
+
// Python package managers
|
|
52
|
+
pip: {
|
|
53
|
+
run: "python -m",
|
|
54
|
+
exec: "python -m",
|
|
55
|
+
install: "pip install",
|
|
56
|
+
installSilent: "pip install -q",
|
|
57
|
+
},
|
|
58
|
+
poetry: {
|
|
59
|
+
run: "poetry run",
|
|
60
|
+
exec: "poetry run",
|
|
61
|
+
install: "poetry install",
|
|
62
|
+
installSilent: "poetry install -q",
|
|
63
|
+
},
|
|
64
|
+
uv: {
|
|
65
|
+
run: "uv run",
|
|
66
|
+
exec: "uvx",
|
|
67
|
+
install: "uv pip install",
|
|
68
|
+
installSilent: "uv pip install -q",
|
|
69
|
+
},
|
|
51
70
|
};
|
|
52
71
|
/**
|
|
53
72
|
* Lockfile to package manager mapping (priority order: bun > yarn > pnpm > npm)
|
|
@@ -59,13 +78,23 @@ const LOCKFILE_PRIORITY = [
|
|
|
59
78
|
{ file: "pnpm-lock.yaml", pm: "pnpm" },
|
|
60
79
|
{ file: "package-lock.json", pm: "npm" },
|
|
61
80
|
];
|
|
81
|
+
/**
|
|
82
|
+
* Python lockfile to package manager mapping (priority order: uv > poetry > pip)
|
|
83
|
+
*/
|
|
84
|
+
const PYTHON_LOCKFILE_PRIORITY = [
|
|
85
|
+
{ file: "uv.lock", pm: "uv" },
|
|
86
|
+
{ file: "poetry.lock", pm: "poetry" },
|
|
87
|
+
// requirements.txt is a fallback when no lockfile is found
|
|
88
|
+
];
|
|
62
89
|
/**
|
|
63
90
|
* Detect package manager from lockfiles
|
|
64
|
-
* Priority: bun > yarn > pnpm > npm
|
|
91
|
+
* Priority: bun > yarn > pnpm > npm (for JS)
|
|
92
|
+
* Priority: uv > poetry > pip (for Python)
|
|
65
93
|
* Falls back to npm if no lockfile found but package.json exists
|
|
94
|
+
* Falls back to pip if no lockfile found but pyproject.toml/requirements.txt exists
|
|
66
95
|
*/
|
|
67
96
|
export async function detectPackageManager() {
|
|
68
|
-
// Check lockfiles in priority order
|
|
97
|
+
// Check JS lockfiles in priority order
|
|
69
98
|
for (const { file, pm } of LOCKFILE_PRIORITY) {
|
|
70
99
|
if (await fileExists(file)) {
|
|
71
100
|
return pm;
|
|
@@ -75,7 +104,18 @@ export async function detectPackageManager() {
|
|
|
75
104
|
if (await fileExists("package.json")) {
|
|
76
105
|
return "npm";
|
|
77
106
|
}
|
|
78
|
-
//
|
|
107
|
+
// Check Python lockfiles in priority order
|
|
108
|
+
for (const { file, pm } of PYTHON_LOCKFILE_PRIORITY) {
|
|
109
|
+
if (await fileExists(file)) {
|
|
110
|
+
return pm;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Fallback to pip if pyproject.toml or requirements.txt exists
|
|
114
|
+
if ((await fileExists("pyproject.toml")) ||
|
|
115
|
+
(await fileExists("requirements.txt"))) {
|
|
116
|
+
return "pip";
|
|
117
|
+
}
|
|
118
|
+
// Not a recognized project type
|
|
79
119
|
return null;
|
|
80
120
|
}
|
|
81
121
|
/**
|
|
@@ -137,10 +137,10 @@ export type ACStatus = z.infer<typeof ACStatusSchema>;
|
|
|
137
137
|
* Acceptance criteria verification method
|
|
138
138
|
*/
|
|
139
139
|
export declare const ACVerificationMethodSchema: z.ZodEnum<{
|
|
140
|
+
manual: "manual";
|
|
140
141
|
unit_test: "unit_test";
|
|
141
142
|
integration_test: "integration_test";
|
|
142
143
|
browser_test: "browser_test";
|
|
143
|
-
manual: "manual";
|
|
144
144
|
}>;
|
|
145
145
|
export type ACVerificationMethod = z.infer<typeof ACVerificationMethodSchema>;
|
|
146
146
|
/**
|
|
@@ -150,10 +150,10 @@ export declare const AcceptanceCriterionSchema: z.ZodObject<{
|
|
|
150
150
|
id: z.ZodString;
|
|
151
151
|
description: z.ZodString;
|
|
152
152
|
verificationMethod: z.ZodEnum<{
|
|
153
|
+
manual: "manual";
|
|
153
154
|
unit_test: "unit_test";
|
|
154
155
|
integration_test: "integration_test";
|
|
155
156
|
browser_test: "browser_test";
|
|
156
|
-
manual: "manual";
|
|
157
157
|
}>;
|
|
158
158
|
status: z.ZodEnum<{
|
|
159
159
|
pending: "pending";
|
|
@@ -173,10 +173,10 @@ export declare const AcceptanceCriteriaSchema: z.ZodObject<{
|
|
|
173
173
|
id: z.ZodString;
|
|
174
174
|
description: z.ZodString;
|
|
175
175
|
verificationMethod: z.ZodEnum<{
|
|
176
|
+
manual: "manual";
|
|
176
177
|
unit_test: "unit_test";
|
|
177
178
|
integration_test: "integration_test";
|
|
178
179
|
browser_test: "browser_test";
|
|
179
|
-
manual: "manual";
|
|
180
180
|
}>;
|
|
181
181
|
status: z.ZodEnum<{
|
|
182
182
|
pending: "pending";
|
|
@@ -252,10 +252,10 @@ export declare const IssueStateSchema: z.ZodObject<{
|
|
|
252
252
|
id: z.ZodString;
|
|
253
253
|
description: z.ZodString;
|
|
254
254
|
verificationMethod: z.ZodEnum<{
|
|
255
|
+
manual: "manual";
|
|
255
256
|
unit_test: "unit_test";
|
|
256
257
|
integration_test: "integration_test";
|
|
257
258
|
browser_test: "browser_test";
|
|
258
|
-
manual: "manual";
|
|
259
259
|
}>;
|
|
260
260
|
status: z.ZodEnum<{
|
|
261
261
|
pending: "pending";
|
|
@@ -377,10 +377,10 @@ export declare const WorkflowStateSchema: z.ZodObject<{
|
|
|
377
377
|
id: z.ZodString;
|
|
378
378
|
description: z.ZodString;
|
|
379
379
|
verificationMethod: z.ZodEnum<{
|
|
380
|
+
manual: "manual";
|
|
380
381
|
unit_test: "unit_test";
|
|
381
382
|
integration_test: "integration_test";
|
|
382
383
|
browser_test: "browser_test";
|
|
383
|
-
manual: "manual";
|
|
384
384
|
}>;
|
|
385
385
|
status: z.ZodEnum<{
|
|
386
386
|
pending: "pending";
|