sequant 1.18.0 → 1.19.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sequant",
3
3
  "description": "Structured workflow system for Claude Code - GitHub issue resolution with spec, exec, test, and QA phases",
4
- "version": "1.18.0",
4
+ "version": "1.19.0",
5
5
  "author": {
6
6
  "name": "sequant-io",
7
7
  "email": "hello@sequant.io"
package/dist/bin/cli.js CHANGED
@@ -45,6 +45,7 @@ import { dashboardCommand } from "../src/commands/dashboard.js";
45
45
  import { stateInitCommand, stateRebuildCommand, stateCleanCommand, } from "../src/commands/state.js";
46
46
  import { syncCommand, areSkillsOutdated } from "../src/commands/sync.js";
47
47
  import { mergeCommand } from "../src/commands/merge.js";
48
+ import { conventionsCommand } from "../src/commands/conventions.js";
48
49
  import { getManifest } from "../src/lib/manifest.js";
49
50
  const program = new Command();
50
51
  // Handle --no-color before parsing
@@ -159,6 +160,12 @@ program
159
160
  .option("--json", "Output as JSON")
160
161
  .option("-v, --verbose", "Enable verbose output")
161
162
  .action(mergeCommand);
163
+ program
164
+ .command("conventions")
165
+ .description("View and manage codebase conventions")
166
+ .option("--detect", "Re-run convention detection")
167
+ .option("--reset", "Clear detected conventions (keep manual)")
168
+ .action(conventionsCommand);
162
169
  program
163
170
  .command("logs")
164
171
  .description("View and analyze workflow run logs")
@@ -0,0 +1,9 @@
1
+ /**
2
+ * sequant conventions - View and manage codebase conventions
3
+ */
4
+ interface ConventionsOptions {
5
+ detect?: boolean;
6
+ reset?: boolean;
7
+ }
8
+ export declare function conventionsCommand(options: ConventionsOptions): Promise<void>;
9
+ export {};
@@ -0,0 +1,61 @@
1
+ /**
2
+ * sequant conventions - View and manage codebase conventions
3
+ */
4
+ import chalk from "chalk";
5
+ import { detectAndSaveConventions, loadConventions, formatConventions, CONVENTIONS_PATH, } from "../lib/conventions-detector.js";
6
+ import { fileExists, writeFile } from "../lib/fs.js";
7
+ export async function conventionsCommand(options) {
8
+ if (options.reset) {
9
+ await handleReset();
10
+ return;
11
+ }
12
+ if (options.detect) {
13
+ await handleDetect();
14
+ return;
15
+ }
16
+ // Default: show current conventions
17
+ await handleShow();
18
+ }
19
+ async function handleDetect() {
20
+ console.log(chalk.blue("Detecting codebase conventions..."));
21
+ const result = await detectAndSaveConventions(process.cwd());
22
+ const count = Object.keys(result.detected).length;
23
+ console.log(chalk.green(`\nDetected ${count} conventions:`));
24
+ console.log(formatConventions(result));
25
+ console.log(chalk.gray(`\nSaved to ${CONVENTIONS_PATH}`));
26
+ }
27
+ async function handleReset() {
28
+ const existing = await loadConventions();
29
+ if (!existing) {
30
+ console.log(chalk.yellow("No conventions file found. Nothing to reset."));
31
+ return;
32
+ }
33
+ // Keep manual entries, clear detected
34
+ const reset = {
35
+ detected: {},
36
+ manual: existing.manual,
37
+ detectedAt: "",
38
+ };
39
+ await writeFile(CONVENTIONS_PATH, JSON.stringify(reset, null, 2));
40
+ console.log(chalk.green("Detected conventions cleared. Manual entries preserved."));
41
+ if (Object.keys(existing.manual).length > 0) {
42
+ console.log(chalk.gray("\nManual entries kept:"));
43
+ for (const [key, value] of Object.entries(existing.manual)) {
44
+ console.log(chalk.gray(` ${key}: ${value}`));
45
+ }
46
+ }
47
+ }
48
+ async function handleShow() {
49
+ if (!(await fileExists(CONVENTIONS_PATH))) {
50
+ console.log(chalk.yellow("No conventions detected yet."));
51
+ console.log(chalk.gray("Run 'sequant conventions --detect' or 'sequant init' to detect conventions."));
52
+ return;
53
+ }
54
+ const conventions = await loadConventions();
55
+ if (!conventions) {
56
+ console.log(chalk.yellow("Could not read conventions file."));
57
+ return;
58
+ }
59
+ console.log(formatConventions(conventions));
60
+ console.log(chalk.gray(`\nEdit ${CONVENTIONS_PATH} to add manual overrides.`));
61
+ }
@@ -9,6 +9,7 @@ import { copyTemplates } from "../lib/templates.js";
9
9
  import { createManifest } from "../lib/manifest.js";
10
10
  import { saveConfig } from "../lib/config.js";
11
11
  import { createDefaultSettings } from "../lib/settings.js";
12
+ import { detectAndSaveConventions } from "../lib/conventions-detector.js";
12
13
  import { fileExists, ensureDir, readFile, writeFile } from "../lib/fs.js";
13
14
  import { commandExists, isGhAuthenticated, getInstallHint, } from "../lib/system.js";
14
15
  import { shouldUseInteractiveMode, getNonInteractiveReason, } from "../lib/tty.js";
@@ -347,6 +348,17 @@ export async function initCommand(options) {
347
348
  settingsSpinner.start();
348
349
  await createDefaultSettings();
349
350
  settingsSpinner.succeed("Created default settings");
351
+ // Detect codebase conventions
352
+ const conventionsSpinner = ui.spinner("Detecting codebase conventions...");
353
+ conventionsSpinner.start();
354
+ try {
355
+ const conventions = await detectAndSaveConventions(process.cwd());
356
+ const count = Object.keys(conventions.detected).length;
357
+ conventionsSpinner.succeed(`Detected ${count} codebase conventions`);
358
+ }
359
+ catch {
360
+ conventionsSpinner.warn("Could not detect conventions (non-blocking)");
361
+ }
350
362
  // Copy templates (with symlinks for scripts unless --no-symlinks)
351
363
  const templatesSpinner = ui.spinner("Copying templates...");
352
364
  templatesSpinner.start();
@@ -0,0 +1,62 @@
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
+ /** Path to conventions file */
8
+ export declare const CONVENTIONS_PATH = ".sequant/conventions.json";
9
+ /**
10
+ * A single detected convention
11
+ */
12
+ export interface Convention {
13
+ /** Machine-readable key */
14
+ key: string;
15
+ /** Human-readable label */
16
+ label: string;
17
+ /** Detected value */
18
+ value: string;
19
+ /** How it was detected */
20
+ source: "detected" | "manual";
21
+ /** What evidence triggered detection */
22
+ evidence?: string;
23
+ }
24
+ /**
25
+ * Full conventions file schema
26
+ */
27
+ export interface ConventionsFile {
28
+ /** Auto-detected conventions */
29
+ detected: Record<string, string>;
30
+ /** User-provided overrides */
31
+ manual: Record<string, string>;
32
+ /** When detection was last run */
33
+ detectedAt: string;
34
+ }
35
+ /**
36
+ * Run all convention detectors
37
+ */
38
+ export declare function detectConventions(projectRoot: string): Promise<Convention[]>;
39
+ /**
40
+ * Load existing conventions file
41
+ */
42
+ export declare function loadConventions(): Promise<ConventionsFile | null>;
43
+ /**
44
+ * Save conventions, preserving manual entries
45
+ */
46
+ export declare function saveConventions(detected: Convention[]): Promise<ConventionsFile>;
47
+ /**
48
+ * Get merged conventions (manual overrides detected)
49
+ */
50
+ export declare function getMergedConventions(file: ConventionsFile): Record<string, string>;
51
+ /**
52
+ * Format conventions for display
53
+ */
54
+ export declare function formatConventions(file: ConventionsFile): string;
55
+ /**
56
+ * Detect and save conventions in one call
57
+ */
58
+ export declare function detectAndSaveConventions(projectRoot: string): Promise<ConventionsFile>;
59
+ /**
60
+ * Format conventions as context for AI skills
61
+ */
62
+ export declare function formatConventionsForContext(file: ConventionsFile): string;
@@ -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
+ }