javi-forge 1.6.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +45 -1
  2. package/ci-local/README.md +56 -11
  3. package/ci-local/ci-local.ps1 +445 -0
  4. package/ci-local/ci-local.sh +52 -9
  5. package/ci-local/docker/python.Dockerfile +7 -0
  6. package/ci-local/hooks/commit-msg +120 -44
  7. package/ci-local/hooks/commit-msg.test.sh +286 -0
  8. package/ci-local/hooks/pre-commit +9 -1
  9. package/ci-local/hooks/pre-push +13 -2
  10. package/ci-local/install.ps1 +367 -0
  11. package/ci-local/install.sh +151 -5
  12. package/ci-local/uninstall.ps1 +85 -0
  13. package/ci-local/uninstall.sh +95 -0
  14. package/dist/cli/dispatch/ci.d.ts +12 -0
  15. package/dist/cli/dispatch/ci.js +38 -0
  16. package/dist/cli/dispatch/security.d.ts +11 -0
  17. package/dist/cli/dispatch/security.js +68 -0
  18. package/dist/cli/dispatch/simple-renderers.d.ts +16 -0
  19. package/dist/cli/dispatch/simple-renderers.js +65 -0
  20. package/dist/cli/dispatch/skill-publish.d.ts +15 -0
  21. package/dist/cli/dispatch/skill-publish.js +44 -0
  22. package/dist/cli/dispatch/skills-cmd.d.ts +13 -0
  23. package/dist/cli/dispatch/skills-cmd.js +93 -0
  24. package/dist/cli/dispatch/tdd.d.ts +11 -0
  25. package/dist/cli/dispatch/tdd.js +45 -0
  26. package/dist/cli/dispatch/types.d.ts +29 -0
  27. package/dist/cli/dispatch/types.js +2 -0
  28. package/dist/cli/dispatch/workflow.d.ts +11 -0
  29. package/dist/cli/dispatch/workflow.js +67 -0
  30. package/dist/cli/help.d.ts +127 -0
  31. package/dist/cli/help.js +138 -0
  32. package/dist/cli/runtime.d.ts +16 -0
  33. package/dist/cli/runtime.js +38 -0
  34. package/dist/cli/validators.d.ts +17 -0
  35. package/dist/cli/validators.js +35 -0
  36. package/dist/commands/analyze.js +1 -3
  37. package/dist/commands/ci.d.ts +47 -0
  38. package/dist/commands/ci.js +380 -63
  39. package/dist/commands/doctor.js +2 -4
  40. package/dist/commands/init/report.d.ts +13 -0
  41. package/dist/commands/init/report.js +13 -0
  42. package/dist/commands/init/steps/agent-skills.d.ts +13 -0
  43. package/dist/commands/init/steps/agent-skills.js +45 -0
  44. package/dist/commands/init/steps/ai-sync.d.ts +15 -0
  45. package/dist/commands/init/steps/ai-sync.js +62 -0
  46. package/dist/commands/init/steps/ci.d.ts +24 -0
  47. package/dist/commands/init/steps/ci.js +74 -0
  48. package/dist/commands/init/steps/claude-md.d.ts +14 -0
  49. package/dist/commands/init/steps/claude-md.js +54 -0
  50. package/dist/commands/init/steps/code-graph.d.ts +16 -0
  51. package/dist/commands/init/steps/code-graph.js +68 -0
  52. package/dist/commands/init/steps/context-dir.d.ts +13 -0
  53. package/dist/commands/init/steps/context-dir.js +47 -0
  54. package/dist/commands/init/steps/docker-deploy.d.ts +16 -0
  55. package/dist/commands/init/steps/docker-deploy.js +60 -0
  56. package/dist/commands/init/steps/ghagga.d.ts +15 -0
  57. package/dist/commands/init/steps/ghagga.js +58 -0
  58. package/dist/commands/init/steps/git.d.ts +25 -0
  59. package/dist/commands/init/steps/git.js +81 -0
  60. package/dist/commands/init/steps/gitignore.d.ts +13 -0
  61. package/dist/commands/init/steps/gitignore.js +39 -0
  62. package/dist/commands/init/steps/local-ai.d.ts +14 -0
  63. package/dist/commands/init/steps/local-ai.js +53 -0
  64. package/dist/commands/init/steps/manifest.d.ts +13 -0
  65. package/dist/commands/init/steps/manifest.js +58 -0
  66. package/dist/commands/init/steps/memory.d.ts +15 -0
  67. package/dist/commands/init/steps/memory.js +60 -0
  68. package/dist/commands/init/steps/mock.d.ts +13 -0
  69. package/dist/commands/init/steps/mock.js +64 -0
  70. package/dist/commands/init/steps/sdd.d.ts +12 -0
  71. package/dist/commands/init/steps/sdd.js +40 -0
  72. package/dist/commands/init/steps/security.d.ts +28 -0
  73. package/dist/commands/init/steps/security.js +103 -0
  74. package/dist/commands/init/types.d.ts +30 -0
  75. package/dist/commands/init/types.js +2 -0
  76. package/dist/commands/init.d.ts +5 -4
  77. package/dist/commands/init.js +42 -630
  78. package/dist/commands/llmstxt.js +1 -1
  79. package/dist/commands/plugin.js +1 -1
  80. package/dist/commands/security.js +23 -4
  81. package/dist/commands/skills/analysis.d.ts +19 -0
  82. package/dist/commands/skills/analysis.js +211 -0
  83. package/dist/commands/skills/benchmark.d.ts +6 -0
  84. package/dist/commands/skills/benchmark.js +82 -0
  85. package/dist/commands/skills/constants.d.ts +10 -0
  86. package/dist/commands/skills/constants.js +27 -0
  87. package/dist/commands/skills/directives.d.ts +21 -0
  88. package/dist/commands/skills/directives.js +101 -0
  89. package/dist/commands/skills/parsing.d.ts +16 -0
  90. package/dist/commands/skills/parsing.js +108 -0
  91. package/dist/commands/skills/rules.d.ts +5 -0
  92. package/dist/commands/skills/rules.js +32 -0
  93. package/dist/commands/skills/scoring.d.ts +47 -0
  94. package/dist/commands/skills/scoring.js +306 -0
  95. package/dist/commands/skills.d.ts +7 -125
  96. package/dist/commands/skills.js +8 -874
  97. package/dist/commands/tdd-pipeline.js +1 -1
  98. package/dist/commands/tdd.js +1 -1
  99. package/dist/commands/workflow.js +2 -0
  100. package/dist/constants.js +2 -2
  101. package/dist/index.js +26 -473
  102. package/dist/lib/agent-skills.js +1 -1
  103. package/dist/{commands → lib}/atlassian-mcp.js +2 -2
  104. package/dist/lib/auto-skill-install.js +2 -3
  105. package/dist/lib/auto-wire.js +7 -7
  106. package/dist/lib/ci-config.d.ts +65 -0
  107. package/dist/lib/ci-config.js +297 -0
  108. package/dist/lib/claudemd.js +1 -1
  109. package/dist/lib/codex-export.js +4 -8
  110. package/dist/lib/common.js +2 -2
  111. package/dist/lib/context.js +2 -2
  112. package/dist/{commands → lib}/crash-recovery.js +4 -4
  113. package/dist/lib/docker.d.ts +22 -1
  114. package/dist/lib/docker.js +57 -49
  115. package/dist/lib/exec.d.ts +10 -0
  116. package/dist/lib/exec.js +11 -0
  117. package/dist/lib/frontmatter.js +2 -2
  118. package/dist/{commands → lib}/harness-audit.js +1 -1
  119. package/dist/{commands → lib}/parallel-batch.js +1 -3
  120. package/dist/lib/plugin.js +2 -4
  121. package/dist/{commands → lib}/secret-scanner.js +3 -1
  122. package/dist/{commands → lib}/security-analysis.js +4 -2
  123. package/dist/lib/skill-publish.js +23 -5
  124. package/dist/{commands → lib}/skill-scanner.d.ts +1 -1
  125. package/dist/{commands → lib}/skill-scanner.js +3 -3
  126. package/dist/lib/stack-detector.js +3 -3
  127. package/dist/{commands → lib}/supply-chain.js +1 -1
  128. package/dist/lib/template.js +2 -2
  129. package/dist/lib/workflow/discovery.d.ts +8 -0
  130. package/dist/lib/workflow/discovery.js +18 -1
  131. package/dist/lib/workflow/parser.js +15 -15
  132. package/dist/lib/workflow/validator.js +4 -4
  133. package/dist/tasks/index.d.ts +3 -3
  134. package/dist/tasks/index.js +3 -3
  135. package/dist/tasks/scaffold-tasks.d.ts +1 -1
  136. package/dist/tasks/scaffold-tasks.js +10 -10
  137. package/dist/tasks/task-id.d.ts +1 -1
  138. package/dist/tasks/task-id.js +9 -5
  139. package/dist/tasks/task-tracker.d.ts +1 -1
  140. package/dist/tasks/task-tracker.js +7 -7
  141. package/dist/ui/App.js +1 -1
  142. package/dist/ui/CIContext.js +1 -2
  143. package/dist/ui/Header.js +4 -4
  144. package/dist/ui/NameInput.js +1 -1
  145. package/dist/ui/Progress.d.ts +1 -1
  146. package/dist/ui/Progress.js +1 -1
  147. package/dist/ui/StackSelector.js +1 -1
  148. package/lib/common.psm1 +167 -0
  149. package/lib/common.sh +30 -0
  150. package/package.json +38 -19
  151. /package/dist/{commands → lib}/atlassian-mcp.d.ts +0 -0
  152. /package/dist/{commands → lib}/crash-recovery.d.ts +0 -0
  153. /package/dist/{commands → lib}/harness-audit.d.ts +0 -0
  154. /package/dist/{commands → lib}/parallel-batch.d.ts +0 -0
  155. /package/dist/{commands → lib}/secret-scanner.d.ts +0 -0
  156. /package/dist/{commands → lib}/security-analysis.d.ts +0 -0
  157. /package/dist/{commands → lib}/supply-chain.d.ts +0 -0
  158. /package/dist/{commands → lib}/team-presets.d.ts +0 -0
  159. /package/dist/{commands → lib}/team-presets.js +0 -0
@@ -1,6 +1,6 @@
1
+ import path from "node:path";
1
2
  import fs from "fs-extra";
2
3
  import { glob } from "glob";
3
- import path from "path";
4
4
  import { detectStack } from "../lib/common.js";
5
5
  function report(onStep, id, label, status, detail) {
6
6
  onStep({ id, label, status, detail });
@@ -136,7 +136,7 @@ export async function runPluginExportCodex(name, onStep) {
136
136
  report(onStep, stepId, `Export plugin as Codex TOML: ${name}`, "running");
137
137
  const result = await exportPluginAsCodexToml(name);
138
138
  if (result.success) {
139
- report(onStep, stepId, `Export plugin as Codex TOML: ${name}`, "done", `exported ${result.files.length} TOML file(s)`);
139
+ report(onStep, stepId, `Export plugin as Codex TOML: ${name}`, "done", `exported ${result.files?.length} TOML file(s)`);
140
140
  }
141
141
  else {
142
142
  report(onStep, stepId, `Export plugin as Codex TOML: ${name}`, "error", result.error);
@@ -1,9 +1,7 @@
1
- import { execFile } from "child_process";
1
+ import path from "node:path";
2
2
  import fs from "fs-extra";
3
- import path from "path";
4
- import { promisify } from "util";
3
+ import { execFileAsync } from "../lib/exec.js";
5
4
  import { detectCIStack } from "./ci.js";
6
- const execFileAsync = promisify(execFile);
7
5
  // =============================================================================
8
6
  // Constants
9
7
  // =============================================================================
@@ -292,6 +290,27 @@ export async function readBaseline(projectDir) {
292
290
  export async function writeBaseline(projectDir, baseline) {
293
291
  const bp = baselinePath(projectDir);
294
292
  await fs.ensureDir(path.dirname(bp));
293
+ // Refuse to follow symlinks: if .javi-forge/security-baseline.json has been
294
+ // replaced with a symlink (to /etc/passwd, /home/user/.ssh/authorized_keys,
295
+ // or any other sensitive file owned by the user) the default fs.writeJson
296
+ // would clobber the target. lstat reports the link, not what it points to.
297
+ let isSymlink = false;
298
+ try {
299
+ const stat = await fs.lstat(bp);
300
+ isSymlink = stat.isSymbolicLink();
301
+ }
302
+ catch (e) {
303
+ const code = e && typeof e === "object" && "code" in e
304
+ ? e.code
305
+ : "";
306
+ if (code !== "ENOENT") {
307
+ throw e;
308
+ }
309
+ // ENOENT is expected on first run; continue.
310
+ }
311
+ if (isSymlink) {
312
+ throw new Error(`Refusing to write baseline through a symlink: ${bp}. Remove the symlink first.`);
313
+ }
295
314
  await fs.writeJson(bp, baseline, { spaces: 2 });
296
315
  }
297
316
  // =============================================================================
@@ -0,0 +1,19 @@
1
+ import type { ConflictKind, SkillBudgetEntry, SkillBudgetResult, SkillBudgetSuggestion, SkillConflict, SkillDuplicate } from "../../types/index.js";
2
+ /**
3
+ * Generate minimal disable sets to bring total tokens under budget.
4
+ * Uses a greedy approach: disable largest skills first until under budget.
5
+ * Returns up to 3 alternative optimization suggestions.
6
+ */
7
+ export declare function generateBudgetOptimizations(entries: SkillBudgetEntry[], totalTokens: number, budget: number): SkillBudgetSuggestion[];
8
+ /** Check if two rules contradict each other (regex pairs + directive clash) */
9
+ export declare function detectRuleConflict(ruleA: string, ruleB: string): {
10
+ reason: string;
11
+ kind: ConflictKind;
12
+ } | null;
13
+ /** Scan all skills for conflicting critical rules */
14
+ export declare function findConflicts(skillsDir: string): Promise<SkillConflict[]>;
15
+ /** Calculate token budget for all installed skills */
16
+ export declare function calculateBudget(skillsDir: string, budget?: number): Promise<SkillBudgetResult>;
17
+ /** Find skills that overlap in scope/triggers */
18
+ export declare function findDuplicates(skillsDir: string): Promise<SkillDuplicate[]>;
19
+ //# sourceMappingURL=analysis.d.ts.map
@@ -0,0 +1,211 @@
1
+ import { CONTRADICTION_PAIRS, DEFAULT_BUDGET } from "./constants.js";
2
+ import { detectDirectiveClash } from "./directives.js";
3
+ import { discoverSkills, estimateTokens, parseSkillFile } from "./parsing.js";
4
+ // ── Budget Optimization ─────────────────────────────────────────────────────
5
+ /**
6
+ * Generate minimal disable sets to bring total tokens under budget.
7
+ * Uses a greedy approach: disable largest skills first until under budget.
8
+ * Returns up to 3 alternative optimization suggestions.
9
+ */
10
+ export function generateBudgetOptimizations(entries, totalTokens, budget) {
11
+ if (totalTokens <= budget)
12
+ return [];
13
+ const excess = totalTokens - budget;
14
+ const suggestions = [];
15
+ // Strategy 1: Greedy — disable largest skills first
16
+ const greedy = greedyDisableSet(entries, excess);
17
+ suggestions.push(makeSuggestion(greedy, entries, totalTokens, budget));
18
+ // Strategy 2: Minimal count — find smallest number of skills to disable
19
+ // (try single-skill solutions first, then pairs)
20
+ const singles = entries.filter((e) => e.tokens >= excess);
21
+ if (singles.length > 0) {
22
+ // Pick the smallest single that still meets budget
23
+ const sorted = [...singles].sort((a, b) => a.tokens - b.tokens);
24
+ const minimal = sorted[0];
25
+ if (minimal.skillName !== greedy[0]?.skillName) {
26
+ suggestions.push(makeSuggestion([minimal], entries, totalTokens, budget));
27
+ }
28
+ }
29
+ // Strategy 3: If we have many small skills, show a "trim many" approach
30
+ // Disable the bottom 50% by token count (many small skills)
31
+ if (entries.length >= 4) {
32
+ const sortedAsc = [...entries].sort((a, b) => a.tokens - b.tokens);
33
+ const halfCount = Math.ceil(sortedAsc.length / 2);
34
+ const bottomHalf = sortedAsc.slice(0, halfCount);
35
+ const saved = bottomHalf.reduce((s, e) => s + e.tokens, 0);
36
+ if (saved >= excess) {
37
+ const trimSet = greedyDisableSet([...bottomHalf].sort((a, b) => b.tokens - a.tokens), excess);
38
+ const names = new Set(trimSet.map((e) => e.skillName));
39
+ const alreadySuggested = suggestions.some((s) => s.disableSkills.length === names.size &&
40
+ s.disableSkills.every((n) => names.has(n)));
41
+ if (!alreadySuggested) {
42
+ suggestions.push(makeSuggestion(trimSet, entries, totalTokens, budget));
43
+ }
44
+ }
45
+ }
46
+ return suggestions;
47
+ }
48
+ /** Greedy: pick largest entries until saved >= excess */
49
+ function greedyDisableSet(entries, excess) {
50
+ const sorted = [...entries].sort((a, b) => b.tokens - a.tokens);
51
+ const result = [];
52
+ let saved = 0;
53
+ for (const entry of sorted) {
54
+ if (saved >= excess)
55
+ break;
56
+ result.push(entry);
57
+ saved += entry.tokens;
58
+ }
59
+ return result;
60
+ }
61
+ function makeSuggestion(disableSet, _allEntries, totalTokens, budget) {
62
+ const tokensSaved = disableSet.reduce((s, e) => s + e.tokens, 0);
63
+ const remaining = totalTokens - tokensSaved;
64
+ return {
65
+ disableSkills: disableSet.map((e) => e.skillName),
66
+ tokensSaved,
67
+ remainingTokens: remaining,
68
+ meetsbudget: remaining <= budget,
69
+ };
70
+ }
71
+ // ── Conflict Detection ──────────────────────────────────────────────────────
72
+ /** Check if two rules contradict each other (regex pairs + directive clash) */
73
+ export function detectRuleConflict(ruleA, ruleB) {
74
+ const normA = ruleA.toLowerCase().trim();
75
+ const normB = ruleB.toLowerCase().trim();
76
+ // Strategy 1: Hardcoded regex pairs (fast, high confidence)
77
+ for (const [patternA, patternB] of CONTRADICTION_PAIRS) {
78
+ if ((patternA.test(normA) && patternB.test(normB)) ||
79
+ (patternB.test(normA) && patternA.test(normB))) {
80
+ return {
81
+ reason: `"${ruleA.slice(0, 60)}" vs "${ruleB.slice(0, 60)}"`,
82
+ kind: "regex-pair",
83
+ };
84
+ }
85
+ }
86
+ // Strategy 2: Semantic directive clash (broader, medium confidence)
87
+ const clashReason = detectDirectiveClash(ruleA, ruleB);
88
+ if (clashReason) {
89
+ return { reason: clashReason, kind: "directive-clash" };
90
+ }
91
+ return null;
92
+ }
93
+ /** Scan all skills for conflicting critical rules */
94
+ export async function findConflicts(skillsDir) {
95
+ const skillPaths = await discoverSkills(skillsDir);
96
+ const allRules = [];
97
+ for (const sp of skillPaths) {
98
+ const parsed = await parseSkillFile(sp);
99
+ if (!parsed)
100
+ continue;
101
+ for (const rule of parsed.rules) {
102
+ allRules.push({
103
+ skillName: parsed.name,
104
+ skillPath: sp,
105
+ rule,
106
+ normalized: rule.toLowerCase().trim(),
107
+ });
108
+ }
109
+ }
110
+ const conflicts = [];
111
+ // Compare every pair (O(n^2) but skill count is small ~20-50)
112
+ for (let i = 0; i < allRules.length; i++) {
113
+ for (let j = i + 1; j < allRules.length; j++) {
114
+ const a = allRules[i];
115
+ const b = allRules[j];
116
+ // Skip rules from the same skill
117
+ if (a.skillName === b.skillName)
118
+ continue;
119
+ const result = detectRuleConflict(a.rule, b.rule);
120
+ if (result) {
121
+ conflicts.push({
122
+ ruleA: a,
123
+ ruleB: b,
124
+ reason: result.reason,
125
+ kind: result.kind,
126
+ });
127
+ }
128
+ }
129
+ }
130
+ return conflicts;
131
+ }
132
+ // ── Context Budget ──────────────────────────────────────────────────────────
133
+ /** Calculate token budget for all installed skills */
134
+ export async function calculateBudget(skillsDir, budget = DEFAULT_BUDGET) {
135
+ const skillPaths = await discoverSkills(skillsDir);
136
+ const entries = [];
137
+ for (const sp of skillPaths) {
138
+ const parsed = await parseSkillFile(sp);
139
+ if (!parsed)
140
+ continue;
141
+ entries.push({
142
+ skillName: parsed.name,
143
+ skillPath: sp,
144
+ tokens: estimateTokens(parsed.rawContent),
145
+ });
146
+ }
147
+ // Sort by token count descending (biggest consumers first)
148
+ entries.sort((a, b) => b.tokens - a.tokens);
149
+ const totalTokens = entries.reduce((sum, e) => sum + e.tokens, 0);
150
+ const overBudget = totalTokens > budget;
151
+ const suggestions = [];
152
+ if (overBudget) {
153
+ const excess = totalTokens - budget;
154
+ suggestions.push(`Over budget by ~${excess} tokens`);
155
+ // Suggest disabling the largest skills until under budget
156
+ let saved = 0;
157
+ for (const entry of entries) {
158
+ if (saved >= excess)
159
+ break;
160
+ suggestions.push(`Consider disabling "${entry.skillName}" (~${entry.tokens} tokens)`);
161
+ saved += entry.tokens;
162
+ }
163
+ }
164
+ const optimizations = generateBudgetOptimizations(entries, totalTokens, budget);
165
+ return {
166
+ entries,
167
+ totalTokens,
168
+ budget,
169
+ overBudget,
170
+ suggestions,
171
+ optimizations,
172
+ };
173
+ }
174
+ // ── Duplicate Detection ─────────────────────────────────────────────────────
175
+ /** Find skills that overlap in scope/triggers */
176
+ export async function findDuplicates(skillsDir) {
177
+ const skillPaths = await discoverSkills(skillsDir);
178
+ const skillData = [];
179
+ for (const sp of skillPaths) {
180
+ const parsed = await parseSkillFile(sp);
181
+ if (!parsed || parsed.triggers.length === 0)
182
+ continue;
183
+ skillData.push({ name: parsed.name, triggers: parsed.triggers });
184
+ }
185
+ const duplicates = [];
186
+ for (let i = 0; i < skillData.length; i++) {
187
+ for (let j = i + 1; j < skillData.length; j++) {
188
+ const a = skillData[i];
189
+ const b = skillData[j];
190
+ const sharedTriggers = a.triggers.filter((t) => b.triggers.some((bt) => bt.includes(t) || t.includes(bt)));
191
+ if (sharedTriggers.length === 0)
192
+ continue;
193
+ const maxTriggers = Math.max(a.triggers.length, b.triggers.length);
194
+ const similarity = maxTriggers > 0
195
+ ? Math.round((sharedTriggers.length / maxTriggers) * 100)
196
+ : 0;
197
+ if (similarity >= 30) {
198
+ duplicates.push({
199
+ skillA: a.name,
200
+ skillB: b.name,
201
+ sharedTriggers,
202
+ similarity,
203
+ });
204
+ }
205
+ }
206
+ }
207
+ // Sort by similarity descending
208
+ duplicates.sort((a, b) => b.similarity - a.similarity);
209
+ return duplicates;
210
+ }
211
+ //# sourceMappingURL=analysis.js.map
@@ -0,0 +1,6 @@
1
+ import type { SkillBenchmarkResult } from "../../types/index.js";
2
+ /**
3
+ * Run structural quality benchmark checks against a skill.
4
+ */
5
+ export declare function benchmarkSkill(skillPath: string): Promise<SkillBenchmarkResult | null>;
6
+ //# sourceMappingURL=benchmark.d.ts.map
@@ -0,0 +1,82 @@
1
+ import path from "node:path";
2
+ import { estimateTokens, parseSkillFile } from "./parsing.js";
3
+ import { ACTION_VERBS, VAGUE_TERMS } from "./rules.js";
4
+ // ── Skill benchmarking ──────────────────────────────────────────────────────
5
+ /**
6
+ * Run structural quality benchmark checks against a skill.
7
+ */
8
+ export async function benchmarkSkill(skillPath) {
9
+ const parsed = await parseSkillFile(skillPath);
10
+ if (!parsed)
11
+ return null;
12
+ const checks = [];
13
+ // Check 1: Has YAML frontmatter with name
14
+ checks.push({
15
+ name: "has-frontmatter-name",
16
+ passed: parsed.name.length > 0 &&
17
+ parsed.name !== path.basename(path.dirname(skillPath)),
18
+ detail: parsed.name.length > 0
19
+ ? `name: ${parsed.name}`
20
+ : "No explicit name in frontmatter",
21
+ });
22
+ // Check 2: Has trigger keywords
23
+ checks.push({
24
+ name: "has-triggers",
25
+ passed: parsed.triggers.length > 0,
26
+ detail: parsed.triggers.length > 0
27
+ ? `${parsed.triggers.length} trigger(s) found`
28
+ : 'No "Trigger:" in description',
29
+ });
30
+ // Check 3: Has critical rules (>= 3)
31
+ checks.push({
32
+ name: "has-critical-rules",
33
+ passed: parsed.rules.length >= 3,
34
+ detail: `${parsed.rules.length} rule(s) found`,
35
+ });
36
+ // Check 4: Rules are actionable (contain verbs)
37
+ const actionableRules = parsed.rules.filter((rule) => ACTION_VERBS.some((verb) => verb.test(rule)));
38
+ checks.push({
39
+ name: "rules-actionable",
40
+ passed: parsed.rules.length > 0 &&
41
+ actionableRules.length / parsed.rules.length >= 0.5,
42
+ detail: `${actionableRules.length}/${parsed.rules.length} rules have action verbs`,
43
+ });
44
+ // Check 5: Has code examples
45
+ const codeBlocks = (parsed.rawContent.match(/```/g) ?? []).length / 2;
46
+ checks.push({
47
+ name: "has-code-examples",
48
+ passed: codeBlocks >= 1,
49
+ detail: `${Math.floor(codeBlocks)} code block(s)`,
50
+ });
51
+ // Check 6: Has structured sections (headings)
52
+ const headings = (parsed.rawContent.match(/^#+\s/gm) ?? []).length;
53
+ checks.push({
54
+ name: "has-sections",
55
+ passed: headings >= 3,
56
+ detail: `${headings} section heading(s)`,
57
+ });
58
+ // Check 7: Token budget reasonable (< 3000 tokens)
59
+ const tokens = estimateTokens(parsed.rawContent);
60
+ checks.push({
61
+ name: "token-budget-ok",
62
+ passed: tokens <= 3000,
63
+ detail: `~${tokens} tokens`,
64
+ });
65
+ // Check 8: No vague terms in rules
66
+ const vagueRules = parsed.rules.filter((rule) => VAGUE_TERMS.some((vague) => vague.test(rule)));
67
+ checks.push({
68
+ name: "no-vague-rules",
69
+ passed: vagueRules.length === 0,
70
+ detail: vagueRules.length > 0
71
+ ? `${vagueRules.length} rule(s) contain vague terms`
72
+ : "All rules are specific",
73
+ });
74
+ const passedCount = checks.filter((c) => c.passed).length;
75
+ const passRate = Math.round((passedCount / checks.length) * 100);
76
+ return {
77
+ skillName: parsed.name,
78
+ checks,
79
+ passRate,
80
+ };
81
+ }
82
+ //# sourceMappingURL=benchmark.js.map
@@ -0,0 +1,10 @@
1
+ export declare const DEFAULT_SKILLS_DIR: string;
2
+ export declare const DEFAULT_BUDGET = 8000;
3
+ /** Approximate tokens per character (GPT/Claude rough average) */
4
+ export declare const CHARS_PER_TOKEN = 4;
5
+ /** Default skill quality scoring threshold */
6
+ export declare const DEFAULT_THRESHOLD = 50;
7
+ /** Default registry quality threshold */
8
+ export declare const DEFAULT_REGISTRY_THRESHOLD = 60;
9
+ export declare const CONTRADICTION_PAIRS: [RegExp, RegExp][];
10
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1,27 @@
1
+ import path from "node:path";
2
+ // ── Default paths and limits ────────────────────────────────────────────────
3
+ export const DEFAULT_SKILLS_DIR = path.join(process.env.HOME ?? "~", ".claude", "skills");
4
+ export const DEFAULT_BUDGET = 8000;
5
+ /** Approximate tokens per character (GPT/Claude rough average) */
6
+ export const CHARS_PER_TOKEN = 4;
7
+ /** Default skill quality scoring threshold */
8
+ export const DEFAULT_THRESHOLD = 50;
9
+ /** Default registry quality threshold */
10
+ export const DEFAULT_REGISTRY_THRESHOLD = 60;
11
+ // ── Contradiction keywords (pairs that signal opposite intent) ───────────────
12
+ export const CONTRADICTION_PAIRS = [
13
+ [/\buse semicolons\b/i, /\bno semicolons\b/i],
14
+ [/\bsemicolons required\b/i, /\bno semicolons\b/i],
15
+ [/\bsingle quotes\b/i, /\bdouble quotes\b/i],
16
+ [/\btabs\b/i, /\bspaces\b/i],
17
+ [/\b2[- ]?spaces?\b/i, /\b4[- ]?spaces?\b/i],
18
+ [/\bclass[- ]?based\b/i, /\bfunctional\b/i],
19
+ [/\bOOP\b/i, /\bfunctional\b/i],
20
+ [/\bmutable\b/i, /\bimmutable\b/i],
21
+ [/\bany\b.*\ballowed\b/i, /\bno any\b/i],
22
+ [/\bdefault export\b/i, /\bnamed export\b/i],
23
+ [/\bnever use\b/i, /\balways use\b/i],
24
+ [/\bavoid\b/i, /\bprefer\b/i],
25
+ [/\bdo not\b/i, /\bmust\b/i],
26
+ ];
27
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1,21 @@
1
+ /** A directive is a sentiment + subject extracted from a rule */
2
+ export interface RuleDirective {
3
+ sentiment: "positive" | "negative";
4
+ subject: string;
5
+ }
6
+ /**
7
+ * Extract a directive (sentiment + subject) from a rule string.
8
+ * Returns null if the rule has no clear directive.
9
+ */
10
+ export declare function extractDirective(rule: string): RuleDirective | null;
11
+ /**
12
+ * Check if two subjects are similar enough to be "about the same thing".
13
+ * Uses simple word-overlap (Jaccard-like) — no external NLP needed.
14
+ */
15
+ export declare function subjectsSimilar(a: string, b: string, threshold?: number): boolean;
16
+ /**
17
+ * Detect a directive clash between two rules:
18
+ * opposite sentiments about the same subject.
19
+ */
20
+ export declare function detectDirectiveClash(ruleA: string, ruleB: string): string | null;
21
+ //# sourceMappingURL=directives.d.ts.map
@@ -0,0 +1,101 @@
1
+ // ── Directive extraction (semantic conflict detection) ─────────────────────
2
+ /**
3
+ * Positive and negative signal patterns.
4
+ * Order matters — first match wins, so more specific patterns go first.
5
+ */
6
+ const POSITIVE_SIGNALS = [
7
+ /\balways use\b/i,
8
+ /\bmust use\b/i,
9
+ /\bprefer\b/i,
10
+ /\balways\b/i,
11
+ /\bmust\b/i,
12
+ /\brequire\b/i,
13
+ /\buse\b/i,
14
+ /\benable\b/i,
15
+ /\bshould\b/i,
16
+ ];
17
+ const NEGATIVE_SIGNALS = [
18
+ /\bnever use\b/i,
19
+ /\bdo not use\b/i,
20
+ /\bdon't use\b/i,
21
+ /\bnever\b/i,
22
+ /\bavoid\b/i,
23
+ /\bdo not\b/i,
24
+ /\bdon't\b/i,
25
+ /\bdisable\b/i,
26
+ /\bno\b/i,
27
+ /\bforbid\b/i,
28
+ ];
29
+ /**
30
+ * Extract a directive (sentiment + subject) from a rule string.
31
+ * Returns null if the rule has no clear directive.
32
+ */
33
+ export function extractDirective(rule) {
34
+ const norm = rule.toLowerCase().trim();
35
+ // Try negative first (more specific: "never use X" before "use X")
36
+ for (const pattern of NEGATIVE_SIGNALS) {
37
+ const match = norm.match(pattern);
38
+ if (match) {
39
+ const subject = norm
40
+ .slice((match.index ?? 0) + match[0].length)
41
+ .trim()
42
+ .replace(/^(the|a|an)\s+/i, "")
43
+ .replace(/[.;,!]+$/, "")
44
+ .trim();
45
+ if (subject.length >= 3) {
46
+ return { sentiment: "negative", subject };
47
+ }
48
+ }
49
+ }
50
+ for (const pattern of POSITIVE_SIGNALS) {
51
+ const match = norm.match(pattern);
52
+ if (match) {
53
+ const subject = norm
54
+ .slice((match.index ?? 0) + match[0].length)
55
+ .trim()
56
+ .replace(/^(the|a|an)\s+/i, "")
57
+ .replace(/[.;,!]+$/, "")
58
+ .trim();
59
+ if (subject.length >= 3) {
60
+ return { sentiment: "positive", subject };
61
+ }
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+ /**
67
+ * Check if two subjects are similar enough to be "about the same thing".
68
+ * Uses simple word-overlap (Jaccard-like) — no external NLP needed.
69
+ */
70
+ export function subjectsSimilar(a, b, threshold = 0.5) {
71
+ const wordsA = new Set(a.split(/\s+/).filter((w) => w.length > 2));
72
+ const wordsB = new Set(b.split(/\s+/).filter((w) => w.length > 2));
73
+ if (wordsA.size === 0 || wordsB.size === 0)
74
+ return false;
75
+ let intersection = 0;
76
+ for (const w of wordsA) {
77
+ if (wordsB.has(w))
78
+ intersection++;
79
+ }
80
+ const union = new Set([...wordsA, ...wordsB]).size;
81
+ return union > 0 && intersection / union >= threshold;
82
+ }
83
+ /**
84
+ * Detect a directive clash between two rules:
85
+ * opposite sentiments about the same subject.
86
+ */
87
+ export function detectDirectiveClash(ruleA, ruleB) {
88
+ const dA = extractDirective(ruleA);
89
+ const dB = extractDirective(ruleB);
90
+ if (!dA || !dB)
91
+ return null;
92
+ if (dA.sentiment === dB.sentiment)
93
+ return null;
94
+ if (subjectsSimilar(dA.subject, dB.subject)) {
95
+ const posRule = dA.sentiment === "positive" ? ruleA : ruleB;
96
+ const negRule = dA.sentiment === "negative" ? ruleA : ruleB;
97
+ return `Directive clash on "${dA.subject}": positive="${posRule.slice(0, 50)}" vs negative="${negRule.slice(0, 50)}"`;
98
+ }
99
+ return null;
100
+ }
101
+ //# sourceMappingURL=directives.js.map
@@ -0,0 +1,16 @@
1
+ /** Estimate token count from a string */
2
+ export declare function estimateTokens(text: string): number;
3
+ /** Read a SKILL.md and extract its name + critical rules section */
4
+ export declare function parseSkillFile(skillPath: string): Promise<{
5
+ name: string;
6
+ rules: string[];
7
+ rawContent: string;
8
+ triggers: string[];
9
+ } | null>;
10
+ /** Extract critical rules from markdown content */
11
+ export declare function extractCriticalRules(content: string): string[];
12
+ /** Extract trigger keywords from a skill description */
13
+ export declare function extractTriggers(description: string): string[];
14
+ /** Discover all SKILL.md files in a skills directory */
15
+ export declare function discoverSkills(skillsDir: string): Promise<string[]>;
16
+ //# sourceMappingURL=parsing.d.ts.map
@@ -0,0 +1,108 @@
1
+ import path from "node:path";
2
+ import fs from "fs-extra";
3
+ import { parseFrontmatter } from "../../lib/frontmatter.js";
4
+ import { CHARS_PER_TOKEN } from "./constants.js";
5
+ // ── Helpers ──────────────────────────────────────────────────────────────────
6
+ /** Estimate token count from a string */
7
+ export function estimateTokens(text) {
8
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
9
+ }
10
+ /** Read a SKILL.md and extract its name + critical rules section */
11
+ export async function parseSkillFile(skillPath) {
12
+ if (!(await fs.pathExists(skillPath)))
13
+ return null;
14
+ const raw = await fs.readFile(skillPath, "utf-8");
15
+ const fm = parseFrontmatter(raw);
16
+ const rawName = fm?.data?.name;
17
+ const name = typeof rawName === "string"
18
+ ? rawName
19
+ : path.basename(path.dirname(skillPath));
20
+ // Extract critical rules — look for "Critical Rules" or numbered list after it
21
+ const rules = extractCriticalRules(fm?.content ?? raw);
22
+ // Extract trigger keywords from description
23
+ const rawDesc = fm?.data?.description;
24
+ const description = typeof rawDesc === "string" ? rawDesc : "";
25
+ const triggers = extractTriggers(description);
26
+ return { name, rules, rawContent: raw, triggers };
27
+ }
28
+ /** Extract critical rules from markdown content */
29
+ export function extractCriticalRules(content) {
30
+ const rules = [];
31
+ // Strategy 1: Find "Critical Rules" or "## Critical Rules" section
32
+ const block1 = extractSection(content, /Critical Rules?/i);
33
+ if (block1) {
34
+ extractListItems(block1, rules);
35
+ }
36
+ // Strategy 2: If no critical rules section, look for rules/conventions in any section
37
+ if (rules.length === 0) {
38
+ const block2 = extractSection(content, /Rules?/i);
39
+ if (block2) {
40
+ extractListItems(block2, rules);
41
+ }
42
+ }
43
+ return rules;
44
+ }
45
+ /** Extract a markdown section body by heading pattern */
46
+ function extractSection(content, headingPattern) {
47
+ const lines = content.split("\n");
48
+ let capturing = false;
49
+ const blockLines = [];
50
+ for (const line of lines) {
51
+ if (capturing) {
52
+ // Stop at next heading
53
+ if (/^#+\s/.test(line) || /^---/.test(line))
54
+ break;
55
+ blockLines.push(line);
56
+ }
57
+ else if (/^#+\s/.test(line) && headingPattern.test(line)) {
58
+ capturing = true;
59
+ }
60
+ }
61
+ return blockLines.length > 0 ? blockLines.join("\n") : null;
62
+ }
63
+ /** Extract numbered or bulleted list items from a markdown block */
64
+ function extractListItems(block, out) {
65
+ const lines = block.split("\n");
66
+ for (const line of lines) {
67
+ const match = line.match(/^\s*(?:\d+[.)]\s+|-\s+|\*\s+)(.+)/);
68
+ if (match) {
69
+ const cleaned = match[1].trim();
70
+ if (cleaned.length > 5)
71
+ out.push(cleaned);
72
+ }
73
+ }
74
+ }
75
+ /** Extract trigger keywords from a skill description */
76
+ export function extractTriggers(description) {
77
+ const triggerMatch = description.match(/Trigger:\s*(.+)/i);
78
+ if (!triggerMatch)
79
+ return [];
80
+ const triggerText = triggerMatch[1];
81
+ // Split on commas, "or", "and", common delimiters
82
+ const keywords = triggerText
83
+ .split(/[,;]|\bor\b/i)
84
+ .map((k) => k
85
+ .trim()
86
+ .toLowerCase()
87
+ .replace(/^when\s+/i, ""))
88
+ .filter((k) => k.length > 2);
89
+ return keywords;
90
+ }
91
+ // ── Core: Scan installed skills ─────────────────────────────────────────────
92
+ /** Discover all SKILL.md files in a skills directory */
93
+ export async function discoverSkills(skillsDir) {
94
+ if (!(await fs.pathExists(skillsDir)))
95
+ return [];
96
+ const entries = await fs.readdir(skillsDir);
97
+ const skillFiles = [];
98
+ for (const entry of entries) {
99
+ if (entry.startsWith(".") || entry.startsWith("_"))
100
+ continue;
101
+ const skillPath = path.join(skillsDir, entry, "SKILL.md");
102
+ if (await fs.pathExists(skillPath)) {
103
+ skillFiles.push(skillPath);
104
+ }
105
+ }
106
+ return skillFiles.sort();
107
+ }
108
+ //# sourceMappingURL=parsing.js.map
@@ -0,0 +1,5 @@
1
+ /** Vague terms that reduce clarity score */
2
+ export declare const VAGUE_TERMS: RegExp[];
3
+ /** Action verbs that indicate actionable rules */
4
+ export declare const ACTION_VERBS: RegExp[];
5
+ //# sourceMappingURL=rules.d.ts.map