@alecsibilia/luca 13.0.0-alpha.1

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 (128) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +47 -0
  3. package/bin/luca.js +3 -0
  4. package/dist/chunks/branch.mjs +47 -0
  5. package/dist/chunks/bun-runtime.mjs +46 -0
  6. package/dist/chunks/checks.mjs +53 -0
  7. package/dist/chunks/claim-verify.mjs +465 -0
  8. package/dist/chunks/classify.mjs +105 -0
  9. package/dist/chunks/confidence.mjs +199 -0
  10. package/dist/chunks/doctor.mjs +158 -0
  11. package/dist/chunks/hook.mjs +696 -0
  12. package/dist/chunks/init.mjs +715 -0
  13. package/dist/chunks/muninndb-health.mjs +66 -0
  14. package/dist/chunks/phase.mjs +38 -0
  15. package/dist/chunks/pr-review.mjs +122 -0
  16. package/dist/chunks/preferences.mjs +61 -0
  17. package/dist/chunks/repair.mjs +111 -0
  18. package/dist/chunks/repo.mjs +58 -0
  19. package/dist/chunks/retro.mjs +86 -0
  20. package/dist/chunks/roadmap.mjs +58 -0
  21. package/dist/chunks/rules.mjs +527 -0
  22. package/dist/chunks/stale-mcp-server.mjs +90 -0
  23. package/dist/chunks/state.mjs +57 -0
  24. package/dist/chunks/stray-local-install.mjs +200 -0
  25. package/dist/chunks/telemetry.mjs +165 -0
  26. package/dist/chunks/todo.mjs +151 -0
  27. package/dist/chunks/vault-init.mjs +300 -0
  28. package/dist/chunks/verification.mjs +95 -0
  29. package/dist/chunks/version.mjs +70 -0
  30. package/dist/chunks/workflow.mjs +47 -0
  31. package/dist/claude/.claude/agents/architect.md +410 -0
  32. package/dist/claude/.claude/agents/build.md +111 -0
  33. package/dist/claude/.claude/agents/discuss.md +93 -0
  34. package/dist/claude/.claude/agents/discussion.md +149 -0
  35. package/dist/claude/.claude/agents/execute.md +416 -0
  36. package/dist/claude/.claude/agents/executor.md +161 -0
  37. package/dist/claude/.claude/agents/fast.md +84 -0
  38. package/dist/claude/.claude/agents/finalize.md +484 -0
  39. package/dist/claude/.claude/agents/learner.md +160 -0
  40. package/dist/claude/.claude/agents/plan-reviewer.md +129 -0
  41. package/dist/claude/.claude/agents/plan.md +96 -0
  42. package/dist/claude/.claude/agents/research.md +327 -0
  43. package/dist/claude/.claude/agents/researcher.md +78 -0
  44. package/dist/claude/.claude/agents/review.md +283 -0
  45. package/dist/claude/.claude/agents/reviewer.md +163 -0
  46. package/dist/claude/.claude/agents/shadow-scanner.md +257 -0
  47. package/dist/claude/.claude/agents/triage.md +230 -0
  48. package/dist/claude/.claude/agents/verifier.md +131 -0
  49. package/dist/claude/.claude/commands/bug-diagnose.md +12 -0
  50. package/dist/claude/.claude/commands/gh-issue-triage.md +14 -0
  51. package/dist/claude/.claude/commands/gh-pr-address.md +235 -0
  52. package/dist/claude/.claude/commands/gh-prepare.md +12 -0
  53. package/dist/claude/.claude/commands/grill-me.md +12 -0
  54. package/dist/claude/.claude/commands/lu-review.md +51 -0
  55. package/dist/claude/.claude/commands/lu.md +75 -0
  56. package/dist/claude/.claude/commands/luca-init.md +14 -0
  57. package/dist/claude/.claude/commands/luca-telemetry-report.md +12 -0
  58. package/dist/claude/.claude/commands/memory-audit.md +12 -0
  59. package/dist/claude/.claude/commands/milestone-new.md +122 -0
  60. package/dist/claude/.claude/commands/phase-discuss.md +45 -0
  61. package/dist/claude/.claude/commands/phase-execute.md +39 -0
  62. package/dist/claude/.claude/commands/phase-plan.md +53 -0
  63. package/dist/claude/.claude/commands/repo-cleanup.md +80 -0
  64. package/dist/claude/.claude/commands/todo-add.md +28 -0
  65. package/dist/claude/.claude/commands/todo-check.md +36 -0
  66. package/dist/claude/.claude/hooks/context-refresher.ts +285 -0
  67. package/dist/claude/.claude/hooks/continuation-messages.ts +215 -0
  68. package/dist/claude/.claude/hooks/pipeline-guard.ts +182 -0
  69. package/dist/claude/.claude/settings.json +41 -0
  70. package/dist/claude/skills/arch-audit/SKILL.md +161 -0
  71. package/dist/claude/skills/autopilot/SKILL.md +1299 -0
  72. package/dist/claude/skills/bug-diagnose/SKILL.md +102 -0
  73. package/dist/claude/skills/choose/SKILL.md +124 -0
  74. package/dist/claude/skills/gh-issue-triage/SKILL.md +97 -0
  75. package/dist/claude/skills/gh-pr-address/SKILL.md +235 -0
  76. package/dist/claude/skills/gh-prepare/SKILL.md +209 -0
  77. package/dist/claude/skills/grill-me/SKILL.md +46 -0
  78. package/dist/claude/skills/lu/SKILL.md +112 -0
  79. package/dist/claude/skills/lu-review/SKILL.md +51 -0
  80. package/dist/claude/skills/luca-init/SKILL.md +91 -0
  81. package/dist/claude/skills/luca-telemetry-report/SKILL.md +145 -0
  82. package/dist/claude/skills/luca-write-surface/SKILL.md +213 -0
  83. package/dist/claude/skills/memory-audit/SKILL.md +217 -0
  84. package/dist/claude/skills/milestone-audit/SKILL.md +545 -0
  85. package/dist/claude/skills/milestone-complete/SKILL.md +168 -0
  86. package/dist/claude/skills/milestone-gaps/SKILL.md +60 -0
  87. package/dist/claude/skills/milestone-new/SKILL.md +125 -0
  88. package/dist/claude/skills/note/SKILL.md +162 -0
  89. package/dist/claude/skills/phase-add/SKILL.md +91 -0
  90. package/dist/claude/skills/phase-assumptions/SKILL.md +92 -0
  91. package/dist/claude/skills/phase-discuss/SKILL.md +165 -0
  92. package/dist/claude/skills/phase-execute/SKILL.md +1786 -0
  93. package/dist/claude/skills/phase-insert/SKILL.md +100 -0
  94. package/dist/claude/skills/phase-plan/SKILL.md +461 -0
  95. package/dist/claude/skills/phase-remove/SKILL.md +113 -0
  96. package/dist/claude/skills/phase-research/SKILL.md +80 -0
  97. package/dist/claude/skills/post-init-tour/SKILL.md +58 -0
  98. package/dist/claude/skills/progress/SKILL.md +271 -0
  99. package/dist/claude/skills/project-new/SKILL.md +609 -0
  100. package/dist/claude/skills/quick/SKILL.md +256 -0
  101. package/dist/claude/skills/rename-audit/SKILL.md +52 -0
  102. package/dist/claude/skills/repo-audit/SKILL.md +88 -0
  103. package/dist/claude/skills/repo-cleanup/SKILL.md +80 -0
  104. package/dist/claude/skills/seed-memory/SKILL.md +235 -0
  105. package/dist/claude/skills/session-pause/SKILL.md +126 -0
  106. package/dist/claude/skills/session-plan/SKILL.md +112 -0
  107. package/dist/claude/skills/session-resume/SKILL.md +75 -0
  108. package/dist/claude/skills/todo-add/SKILL.md +85 -0
  109. package/dist/claude/skills/todo-check/SKILL.md +77 -0
  110. package/dist/claude/skills/workflow-save/SKILL.md +277 -0
  111. package/dist/index.d.mts +33 -0
  112. package/dist/index.d.ts +33 -0
  113. package/dist/index.mjs +69 -0
  114. package/dist/shared/luca.B3Mimc0P.mjs +52 -0
  115. package/dist/shared/luca.B3saVjJm.mjs +163 -0
  116. package/dist/shared/luca.BYdjkfnz.mjs +217 -0
  117. package/dist/shared/luca.BmhNkYe2.mjs +56 -0
  118. package/dist/shared/luca.C4gMUoBd.mjs +358 -0
  119. package/dist/shared/luca.CQ3g1xrD.mjs +19 -0
  120. package/dist/shared/luca.CRmaAfXR.mjs +713 -0
  121. package/dist/shared/luca.CrXzXueR.mjs +57 -0
  122. package/dist/shared/luca.DTomPq7I.mjs +91 -0
  123. package/dist/shared/luca.DjDTeDCi.mjs +1904 -0
  124. package/dist/shared/luca.HZxBTBgD.mjs +201 -0
  125. package/dist/shared/luca.TSMg1t7I.mjs +10 -0
  126. package/dist/shared/luca.dM-MKlNE.mjs +25 -0
  127. package/dist/shared/luca.naWEcQ4B.mjs +7 -0
  128. package/package.json +76 -0
@@ -0,0 +1,527 @@
1
+ import { defineCommand } from 'citty';
2
+ import { join as join$1 } from 'pathe';
3
+ import '../shared/luca.CRmaAfXR.mjs';
4
+ import { existsSync, readdirSync, statSync, readFileSync } from 'node:fs';
5
+ import 'node:fs/promises';
6
+ import { isAbsolute, resolve, join, extname } from 'node:path';
7
+ import 'node:crypto';
8
+ import { createRequire } from 'node:module';
9
+ import { pathToFileURL } from 'node:url';
10
+ import 'node:child_process';
11
+ import { a as analyzeRun, g as gatherRunArtifacts } from '../shared/luca.C4gMUoBd.mjs';
12
+ import { l as listRuns } from '../shared/luca.HZxBTBgD.mjs';
13
+ import { l as logger } from '../shared/luca.dM-MKlNE.mjs';
14
+ import 'zod';
15
+ import 'node:os';
16
+ import '../shared/luca.BmhNkYe2.mjs';
17
+ import '../shared/luca.TSMg1t7I.mjs';
18
+ import 'consola';
19
+
20
+ let tsModuleCache = void 0;
21
+ function resolveTypeScript() {
22
+ if (tsModuleCache !== void 0) return tsModuleCache;
23
+ try {
24
+ const req = createRequire(import.meta.url);
25
+ tsModuleCache = req("typescript");
26
+ } catch {
27
+ tsModuleCache = null;
28
+ }
29
+ return tsModuleCache;
30
+ }
31
+ function walkDir(dir, extensions, out = []) {
32
+ if (!existsSync(dir)) return out;
33
+ let entries;
34
+ try {
35
+ entries = readdirSync(dir);
36
+ } catch {
37
+ return out;
38
+ }
39
+ for (const name of entries) {
40
+ if (name.startsWith(".") || name === "node_modules") continue;
41
+ const full = join(dir, name);
42
+ let stats;
43
+ try {
44
+ stats = statSync(full);
45
+ } catch {
46
+ continue;
47
+ }
48
+ if (stats.isDirectory()) {
49
+ walkDir(full, extensions, out);
50
+ } else if (extensions.includes(extname(name))) {
51
+ if (name.endsWith(".test.ts")) continue;
52
+ if (name.endsWith(".test.js")) continue;
53
+ if (name.endsWith(".spec.ts")) continue;
54
+ if (name.endsWith(".spec.js")) continue;
55
+ out.push(full);
56
+ }
57
+ }
58
+ return out;
59
+ }
60
+ function isRuleDefinition(value) {
61
+ if (!value || typeof value !== "object") return false;
62
+ const v = value;
63
+ return typeof v.id === "string" && typeof v.severity === "string" && typeof v.description === "string" && typeof v.check === "function" && v.scope !== void 0 && v.scope !== null;
64
+ }
65
+ function extractRules(module) {
66
+ if (!module || typeof module !== "object") return [];
67
+ const collected = [];
68
+ const visit = (value) => {
69
+ if (isRuleDefinition(value)) {
70
+ collected.push(value);
71
+ return;
72
+ }
73
+ if (Array.isArray(value)) {
74
+ for (const v of value) visit(v);
75
+ return;
76
+ }
77
+ };
78
+ for (const key of Object.keys(module)) {
79
+ visit(module[key]);
80
+ }
81
+ return collected;
82
+ }
83
+ async function loadRules(opts) {
84
+ const { rulesDir } = opts;
85
+ const loadErrors = [];
86
+ const files = walkDir(rulesDir, [".ts", ".mts", ".js", ".mjs"]);
87
+ const rules = [];
88
+ const seenIds = /* @__PURE__ */ new Set();
89
+ for (const file of files) {
90
+ try {
91
+ const url = pathToFileURL(file).href;
92
+ const mod = await import(url);
93
+ for (const rule of extractRules(mod)) {
94
+ if (seenIds.has(rule.id)) {
95
+ loadErrors.push({
96
+ file,
97
+ message: `duplicate rule id: ${rule.id} (already defined elsewhere)`
98
+ });
99
+ continue;
100
+ }
101
+ seenIds.add(rule.id);
102
+ rules.push(rule);
103
+ }
104
+ } catch (err) {
105
+ loadErrors.push({
106
+ file,
107
+ message: err instanceof Error ? err.message : String(err)
108
+ });
109
+ }
110
+ }
111
+ return { rules, filesDiscovered: files.length, loadErrors };
112
+ }
113
+ function asArray(value) {
114
+ if (value === void 0) return [];
115
+ return Array.isArray(value) ? value : [value];
116
+ }
117
+ function resolveScope(opts) {
118
+ const { repoRoot, scope, exclude } = opts;
119
+ if (scope === "repo") return [""];
120
+ const globs = asArray(scope);
121
+ if (globs.length === 0) return [];
122
+ const collected = /* @__PURE__ */ new Set();
123
+ const BunGlobal = globalThis.Bun;
124
+ if (!BunGlobal?.Glob) {
125
+ throw new Error(
126
+ "rules-runner: Bun runtime required for glob scope resolution"
127
+ );
128
+ }
129
+ for (const pattern of globs) {
130
+ const glob = new BunGlobal.Glob(pattern);
131
+ for (const match of glob.scanSync({ cwd: repoRoot, onlyFiles: true })) {
132
+ collected.add(match);
133
+ }
134
+ }
135
+ if (exclude.length > 0) {
136
+ const excludeMatches = /* @__PURE__ */ new Set();
137
+ for (const pattern of exclude) {
138
+ const glob = new BunGlobal.Glob(pattern);
139
+ for (const match of glob.scanSync({
140
+ cwd: repoRoot,
141
+ onlyFiles: true
142
+ })) {
143
+ excludeMatches.add(match);
144
+ }
145
+ }
146
+ for (const path of collected) {
147
+ if (excludeMatches.has(path)) collected.delete(path);
148
+ }
149
+ }
150
+ return [...collected];
151
+ }
152
+ function scriptKindFor(tsMod, path) {
153
+ const ext = extname(path).toLowerCase();
154
+ switch (ext) {
155
+ case ".ts":
156
+ return tsMod.ScriptKind.TS;
157
+ case ".tsx":
158
+ return tsMod.ScriptKind.TSX;
159
+ case ".js":
160
+ case ".mjs":
161
+ case ".cjs":
162
+ return tsMod.ScriptKind.JS;
163
+ case ".jsx":
164
+ return tsMod.ScriptKind.JSX;
165
+ case ".json":
166
+ return tsMod.ScriptKind.JSON;
167
+ default:
168
+ return tsMod.ScriptKind.Unknown;
169
+ }
170
+ }
171
+ function makeRuleFile(opts) {
172
+ const { repoRoot, relPath, contentCache, astCache } = opts;
173
+ const absolutePath = resolve(repoRoot, relPath);
174
+ let content = contentCache.get(relPath);
175
+ if (content === void 0) {
176
+ try {
177
+ content = readFileSync(absolutePath, "utf-8");
178
+ } catch {
179
+ return null;
180
+ }
181
+ contentCache.set(relPath, content);
182
+ }
183
+ const fileContent = content;
184
+ return {
185
+ path: relPath,
186
+ absolutePath,
187
+ content: fileContent,
188
+ ast() {
189
+ if (astCache.has(relPath)) return astCache.get(relPath) ?? null;
190
+ const tsMod = resolveTypeScript();
191
+ if (!tsMod) {
192
+ astCache.set(relPath, null);
193
+ return null;
194
+ }
195
+ const kind = scriptKindFor(tsMod, relPath);
196
+ if (kind === tsMod.ScriptKind.Unknown) {
197
+ astCache.set(relPath, null);
198
+ return null;
199
+ }
200
+ try {
201
+ const sf = tsMod.createSourceFile(
202
+ relPath,
203
+ fileContent,
204
+ tsMod.ScriptTarget.Latest,
205
+ /* setParentNodes */
206
+ true,
207
+ kind
208
+ );
209
+ astCache.set(relPath, sf);
210
+ return sf;
211
+ } catch {
212
+ astCache.set(relPath, null);
213
+ return null;
214
+ }
215
+ }
216
+ };
217
+ }
218
+ function runRules(opts) {
219
+ const { repoRoot, rules } = opts;
220
+ const findings = [];
221
+ const executionErrors = [];
222
+ const timings = {};
223
+ const contentCache = /* @__PURE__ */ new Map();
224
+ const astCache = /* @__PURE__ */ new Map();
225
+ for (const rule of rules) {
226
+ const start = performance.now();
227
+ const exclude = asArray(rule.exclude);
228
+ let candidates;
229
+ try {
230
+ candidates = resolveScope({ repoRoot, scope: rule.scope, exclude });
231
+ } catch (err) {
232
+ executionErrors.push({
233
+ ruleId: rule.id,
234
+ path: "",
235
+ message: `scope resolution failed: ${err instanceof Error ? err.message : String(err)}`
236
+ });
237
+ timings[rule.id] = performance.now() - start;
238
+ continue;
239
+ }
240
+ for (const relPath of candidates) {
241
+ const file = relPath === "" ? {
242
+ path: "",
243
+ absolutePath: repoRoot,
244
+ content: "",
245
+ ast: () => null
246
+ } : makeRuleFile({
247
+ repoRoot,
248
+ relPath,
249
+ contentCache,
250
+ astCache
251
+ });
252
+ if (!file) continue;
253
+ try {
254
+ const ruleFindings = rule.check(file);
255
+ if (Array.isArray(ruleFindings)) {
256
+ for (const finding of ruleFindings) {
257
+ if (!finding.category && rule.category) {
258
+ finding.category = rule.category;
259
+ }
260
+ findings.push(finding);
261
+ }
262
+ }
263
+ } catch (err) {
264
+ executionErrors.push({
265
+ ruleId: rule.id,
266
+ path: relPath,
267
+ message: err instanceof Error ? err.message : String(err)
268
+ });
269
+ }
270
+ }
271
+ timings[rule.id] = performance.now() - start;
272
+ }
273
+ return { timings, findings, executionErrors };
274
+ }
275
+ async function discoverAndRun(opts) {
276
+ const repoRoot = isAbsolute(opts.repoRoot) ? opts.repoRoot : resolve(opts.repoRoot);
277
+ const rulesDir = opts.rulesDir ? isAbsolute(opts.rulesDir) ? opts.rulesDir : resolve(repoRoot, opts.rulesDir) : join(repoRoot, ".luca", "rules");
278
+ const { rules, filesDiscovered, loadErrors } = await loadRules({ rulesDir });
279
+ const { timings, findings, executionErrors } = runRules({ repoRoot, rules });
280
+ return {
281
+ rulesFilesDiscovered: filesDiscovered,
282
+ rulesLoaded: rules.length,
283
+ timings,
284
+ findings,
285
+ loadErrors,
286
+ executionErrors
287
+ };
288
+ }
289
+
290
+ const DEFAULT_THRESHOLD = 3;
291
+ function codeToSlug(code) {
292
+ return code.toLowerCase().replace(/_/g, "-");
293
+ }
294
+ function detectRecurringPitfalls(opts) {
295
+ const threshold = opts.threshold ?? DEFAULT_THRESHOLD;
296
+ const stats = /* @__PURE__ */ new Map();
297
+ for (const report of opts.reports) {
298
+ for (const v of report.violations) {
299
+ let entry = stats.get(v.code);
300
+ if (!entry) {
301
+ entry = {
302
+ runIds: /* @__PURE__ */ new Set(),
303
+ occurrences: 0,
304
+ sampleMessage: v.message
305
+ };
306
+ stats.set(v.code, entry);
307
+ }
308
+ entry.runIds.add(report.runId);
309
+ entry.occurrences += 1;
310
+ entry.sampleMessage = v.message;
311
+ }
312
+ }
313
+ const recurring = [];
314
+ for (const [code, entry] of stats) {
315
+ if (entry.runIds.size < threshold) continue;
316
+ const slug = codeToSlug(code);
317
+ recurring.push({
318
+ code,
319
+ runCount: entry.runIds.size,
320
+ occurrences: entry.occurrences,
321
+ runIds: [...entry.runIds],
322
+ sampleMessage: entry.sampleMessage,
323
+ suggestedRuleId: `recurring/${slug}`,
324
+ pitfallConcept: `pitfall:${slug}`
325
+ });
326
+ }
327
+ recurring.sort((a, b) => b.runCount - a.runCount);
328
+ return { runsScanned: opts.reports.length, threshold, recurring };
329
+ }
330
+ function renderDraftRule(pitfall) {
331
+ const sample = pitfall.sampleMessage.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
332
+ return `/**
333
+ * Auto-suggested rule from luca recurrence detection.
334
+ *
335
+ * This pitfall has appeared in ${pitfall.runCount} distinct run(s)
336
+ * (${pitfall.occurrences} total occurrence(s)).
337
+ *
338
+ * Sample violation message:
339
+ * ${sample}
340
+ *
341
+ * NEXT STEPS:
342
+ * 1. Decide what code pattern this rule should catch.
343
+ * 2. Implement the matcher in the \`check\` function below.
344
+ * - Use \`file.content\` for regex checks.
345
+ * - Use \`file.ast()\` for AST-level matching.
346
+ * 3. Set \`scope\` to the glob of files this rule should run against.
347
+ * 4. Refine the severity (defaults to 'should-fix').
348
+ * 5. Delete this comment block once the rule is real.
349
+ *
350
+ * The rule is exported as a plain duck-typed object so it works in any
351
+ * consumer repo without a runtime dependency on the harness package.
352
+ */
353
+
354
+ export default {
355
+ id: '${pitfall.suggestedRuleId}',
356
+ severity: 'should-fix',
357
+ description: '${pitfall.code}: ${sample.replace(/'/g, "\\'")}',
358
+ scope: 'src/**/*.ts',
359
+ category: 'recurring',
360
+ check: (file) => {
361
+ // TODO: implement the check.
362
+ // Example (regex):
363
+ // const findings = []
364
+ // const re = /badPattern/g
365
+ // let match
366
+ // while ((match = re.exec(file.content)) !== null) {
367
+ // const line = file.content.slice(0, match.index).split('\\n').length
368
+ // findings.push({
369
+ // id: \`${pitfall.suggestedRuleId}:\${file.path}:\${line}\`,
370
+ // path: file.path,
371
+ // line,
372
+ // severity: 'should-fix',
373
+ // summary: 'Recurring pitfall detected',
374
+ // })
375
+ // }
376
+ // return findings
377
+ return []
378
+ },
379
+ }
380
+ `;
381
+ }
382
+ function renderSuggestedRulesMarkdown(report) {
383
+ if (report.recurring.length === 0) {
384
+ return `# Suggested Rules
385
+
386
+ No recurring pitfalls met the threshold (>= ${report.threshold} runs).
387
+ Scanned ${report.runsScanned} run(s).
388
+ `;
389
+ }
390
+ const sections = report.recurring.map(
391
+ (p) => `## ${p.code} \u2014 ${p.runCount} run(s), ${p.occurrences} occurrence(s)
392
+
393
+ **Suggested rule id**: \`${p.suggestedRuleId}\`
394
+ **Pitfall concept**: \`${p.pitfallConcept}\`
395
+ **Sample message**: ${p.sampleMessage}
396
+
397
+ **Runs where this appeared**:
398
+ ${p.runIds.map((id) => `- ${id}`).join("\n")}
399
+
400
+ **Draft rule** \u2014 copy to \`.luca/rules/${codeToSlug(p.code)}.ts\` and fill in the matcher:
401
+
402
+ \`\`\`ts
403
+ ${renderDraftRule(p)}\`\`\`
404
+ `
405
+ );
406
+ return `# Suggested Rules
407
+
408
+ Recurring pitfalls detected at threshold >= ${report.threshold} runs (out of ${report.runsScanned} scanned).
409
+
410
+ These suggestions are **drafts**. Each is a starting template, not an automatic addition. Review the sample messages, decide whether the pattern is mechanically detectable, fill in the matcher, and commit the rule to \`.luca/rules/\`.
411
+
412
+ ---
413
+
414
+ ${sections.join("\n---\n\n")}`;
415
+ }
416
+
417
+ function reportFindings(report) {
418
+ logger.info(
419
+ `Discovered ${report.rulesFilesDiscovered} rule file(s), loaded ${report.rulesLoaded} rule(s).`
420
+ );
421
+ for (const e of report.loadErrors) {
422
+ logger.error(`load error in ${e.file}: ${e.message}`);
423
+ }
424
+ for (const e of report.executionErrors) {
425
+ logger.error(
426
+ `rule '${e.ruleId}' failed${e.path ? ` on ${e.path}` : ""}: ${e.message}`
427
+ );
428
+ }
429
+ for (const f of report.findings) {
430
+ const loc = f.line !== void 0 ? `${f.path || "<repo>"}:${f.line}` : f.path || "<repo>";
431
+ logger.info(` ${f.severity} ${loc} ${f.summary}`);
432
+ }
433
+ if (report.findings.length === 0) {
434
+ logger.success("No rule findings.");
435
+ } else {
436
+ logger.warn(`${report.findings.length} rule finding(s).`);
437
+ }
438
+ }
439
+ const listCommand = defineCommand({
440
+ meta: {
441
+ name: "list",
442
+ description: "List the rules discovered under .luca/rules/."
443
+ },
444
+ async run() {
445
+ const { rules, filesDiscovered, loadErrors } = await loadRules({
446
+ rulesDir: join$1(process.cwd(), ".luca", "rules")
447
+ });
448
+ for (const e of loadErrors) {
449
+ logger.error(`load error in ${e.file}: ${e.message}`);
450
+ }
451
+ if (rules.length === 0) {
452
+ logger.info(
453
+ `No rules found (${filesDiscovered} file(s) under .luca/rules/).`
454
+ );
455
+ return;
456
+ }
457
+ for (const r of rules) {
458
+ logger.info(`${r.id} [${r.severity}] ${r.description}`);
459
+ }
460
+ }
461
+ });
462
+ const runCommand = defineCommand({
463
+ meta: {
464
+ name: "run",
465
+ description: "Run every repo-local rule pack and report its findings."
466
+ },
467
+ async run() {
468
+ reportFindings(await discoverAndRun({ repoRoot: process.cwd() }));
469
+ }
470
+ });
471
+ const gateCommand = defineCommand({
472
+ meta: {
473
+ name: "gate",
474
+ description: "Run every rule; exit non-zero if any must-fix finding is produced."
475
+ },
476
+ async run() {
477
+ const report = await discoverAndRun({ repoRoot: process.cwd() });
478
+ reportFindings(report);
479
+ const mustFix = report.findings.filter(
480
+ (f) => f.severity === "must-fix"
481
+ );
482
+ if (mustFix.length > 0) {
483
+ logger.error(
484
+ `${mustFix.length} must-fix finding(s) \u2014 rule gate failed.`
485
+ );
486
+ process.exitCode = 1;
487
+ }
488
+ }
489
+ });
490
+ const suggestCommand = defineCommand({
491
+ meta: {
492
+ name: "suggest",
493
+ description: "Surface postmortem pitfalls that recurred across runs as draft rules."
494
+ },
495
+ args: {
496
+ threshold: {
497
+ type: "string",
498
+ description: "Distinct-run recurrence threshold (default 3)."
499
+ }
500
+ },
501
+ run({ args }) {
502
+ const cwd = process.cwd();
503
+ const reports = listRuns({ cwd }).map(
504
+ (r) => analyzeRun(gatherRunArtifacts({ cwd, runId: r.runId }))
505
+ );
506
+ const threshold = args.threshold !== void 0 && Number.isFinite(Number(args.threshold)) ? Number(args.threshold) : void 0;
507
+ const recurrence = detectRecurringPitfalls({ reports, threshold });
508
+ process.stdout.write(
509
+ `${renderSuggestedRulesMarkdown(recurrence)}
510
+ `
511
+ );
512
+ }
513
+ });
514
+ const rulesCommand = defineCommand({
515
+ meta: {
516
+ name: "rules",
517
+ description: "Repo-local rule packs (.luca/rules/)."
518
+ },
519
+ subCommands: {
520
+ list: listCommand,
521
+ run: runCommand,
522
+ gate: gateCommand,
523
+ suggest: suggestCommand
524
+ }
525
+ });
526
+
527
+ export { rulesCommand };
@@ -0,0 +1,90 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+
4
+ const CHECK_NAME = "MCP server registration";
5
+ function isLucaMcpEntry(entry) {
6
+ if (entry === null || typeof entry !== "object") return false;
7
+ const { command, args } = entry;
8
+ if (command !== "luca") return false;
9
+ return Array.isArray(args) && args.map(String).includes("mcp");
10
+ }
11
+ async function readJsonObject(path) {
12
+ try {
13
+ const file = Bun.file(path);
14
+ if (!await file.exists()) return null;
15
+ const parsed = JSON.parse(await file.text());
16
+ return parsed !== null && typeof parsed === "object" ? parsed : null;
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+ function lucaServerKeys(mcpServers) {
22
+ if (mcpServers === null || typeof mcpServers !== "object") return [];
23
+ return Object.entries(mcpServers).filter(([, value]) => isLucaMcpEntry(value)).map(([key]) => key);
24
+ }
25
+ const staleMcpServerCheck = {
26
+ name: CHECK_NAME,
27
+ scope: "prerequisites",
28
+ async run() {
29
+ const cwd = process.cwd();
30
+ const findings = [];
31
+ let inertSettingsEntry = false;
32
+ const mcpJson = await readJsonObject(join(cwd, ".mcp.json"));
33
+ for (const key of lucaServerKeys(mcpJson?.mcpServers)) {
34
+ findings.push(`.mcp.json (mcpServers.${key})`);
35
+ }
36
+ const settings = await readJsonObject(
37
+ join(cwd, ".claude", "settings.json")
38
+ );
39
+ for (const key of lucaServerKeys(settings?.mcpServers)) {
40
+ findings.push(`.claude/settings.json (mcpServers.${key}, inert)`);
41
+ inertSettingsEntry = true;
42
+ }
43
+ const userConfig = await readJsonObject(join(homedir(), ".claude.json"));
44
+ for (const key of lucaServerKeys(userConfig?.mcpServers)) {
45
+ findings.push(`~/.claude.json (mcpServers.${key})`);
46
+ }
47
+ const projects = userConfig?.projects;
48
+ if (projects !== null && typeof projects === "object") {
49
+ const project = projects[cwd];
50
+ if (project !== null && typeof project === "object") {
51
+ for (const key of lucaServerKeys(
52
+ project.mcpServers
53
+ )) {
54
+ findings.push(
55
+ `~/.claude.json (projects[cwd].mcpServers.${key})`
56
+ );
57
+ }
58
+ }
59
+ }
60
+ if (findings.length === 0) {
61
+ return {
62
+ name: CHECK_NAME,
63
+ status: "pass",
64
+ message: "no stale luca MCP server registration",
65
+ fixCommand: null,
66
+ details: null
67
+ };
68
+ }
69
+ const detailLines = [
70
+ "`luca mcp serve` was removed in v13 \u2014 the write surface is now",
71
+ "the `luca` CLI plus the native Write tool. Stale registration in:",
72
+ ...findings.map((finding) => `- ${finding}`)
73
+ ];
74
+ if (inertSettingsEntry) {
75
+ detailLines.push(
76
+ "`claude mcp remove luca` clears .mcp.json and ~/.claude.json.",
77
+ "The .claude/settings.json block is inert \u2014 remove it by hand."
78
+ );
79
+ }
80
+ return {
81
+ name: CHECK_NAME,
82
+ status: "warning",
83
+ message: "stale luca MCP server still registered (removed in v13)",
84
+ fixCommand: "claude mcp remove luca",
85
+ details: detailLines.join("\n ")
86
+ };
87
+ }
88
+ };
89
+
90
+ export { staleMcpServerCheck };
@@ -0,0 +1,57 @@
1
+ import { defineCommand } from 'citty';
2
+ import 'zod';
3
+ import '../shared/luca.CRmaAfXR.mjs';
4
+ import 'node:fs';
5
+ import 'node:fs/promises';
6
+ import 'node:path';
7
+ import 'node:crypto';
8
+ import 'node:module';
9
+ import 'node:url';
10
+ import 'node:child_process';
11
+ import { r as runWriteHandler, l as lucaStateAdvanceTool, a as lucaStateReadTool } from '../shared/luca.DjDTeDCi.mjs';
12
+ import 'node:os';
13
+ import '../shared/luca.CrXzXueR.mjs';
14
+ import '../shared/luca.HZxBTBgD.mjs';
15
+ import '../shared/luca.TSMg1t7I.mjs';
16
+ import '../shared/luca.CQ3g1xrD.mjs';
17
+ import '../shared/luca.B3Mimc0P.mjs';
18
+
19
+ const readCommand = defineCommand({
20
+ meta: {
21
+ name: "read",
22
+ description: "Read the current workflow state from .luca/state.json \u2014 pipelineStep, currentPhase, iteration counters, and roadmap. Pure read; allowed in every pipelineStep."
23
+ },
24
+ async run() {
25
+ await runWriteHandler("state read", lucaStateReadTool, {});
26
+ }
27
+ });
28
+ const advanceCommand = defineCommand({
29
+ meta: {
30
+ name: "advance",
31
+ description: "Atomically advance the workflow pipelineStep. The transition is validated against the pipeline-transitions table; illegal jumps are rejected."
32
+ },
33
+ args: {
34
+ "to-step": {
35
+ type: "string",
36
+ required: true,
37
+ description: "Target pipelineStep (e.g. research, plan, execute). Must be a legal transition from the current step."
38
+ }
39
+ },
40
+ async run({ args }) {
41
+ await runWriteHandler("state advance", lucaStateAdvanceTool, {
42
+ toStep: args["to-step"]
43
+ });
44
+ }
45
+ });
46
+ const stateCommand = defineCommand({
47
+ meta: {
48
+ name: "state",
49
+ description: "Read and advance the Luca workflow state machine"
50
+ },
51
+ subCommands: {
52
+ read: readCommand,
53
+ advance: advanceCommand
54
+ }
55
+ });
56
+
57
+ export { stateCommand };