Skip to content

Commit 2bffb05

Browse files
MShipleyDPclaude
andcommitted
fix: ship plugin references/ and scripts/ with Codex skills
Codex skill generation (gen-adapters.js) and installation (dev-install.js) emitted only SKILL.md, so plugin-level references/ and scripts/ never reached ~/.codex/skills — deslop's SKILL.md pointed at references/slop-categories.md and scripts/detect.js that were absent from every install. Both paths now copy those directories for the plugin's primary skill (skillName === plugin). detect.js additionally resolves lib/ with a fallback to the installed plugin copy (~/.agentsys/plugins/deslop/lib) so the shipped script runs from a skill directory that has no lib/ beside it. Content tests scoped to SKILL.md files (bundled resources are verbatim copies, exempt from frontmatter/placeholder rules). gen-adapters 51/51, dev-install 30/30. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 718a709 commit 2bffb05

6 files changed

Lines changed: 312 additions & 4 deletions

File tree

__tests__/gen-adapters.test.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,9 @@ describe('gen-adapters', () => {
340340
test('generated files start with frontmatter (no header before ---)', () => {
341341
const { files } = genAdapters.computeAdapters();
342342
for (const [filePath, content] of files) {
343+
// Bundled skill resources (references/, scripts/) are copied verbatim
344+
// and are not subject to the frontmatter rule
345+
if (/\/(references|scripts)\//.test(filePath)) continue;
343346
if (content.includes('---\n')) {
344347
// Files with frontmatter must start with --- on line 1
345348
// (no auto-generated header before frontmatter)
@@ -382,7 +385,7 @@ describe('gen-adapters', () => {
382385

383386
test('Codex skill files use placeholder path', () => {
384387
const { files } = genAdapters.computeAdapters();
385-
const codexPaths = [...files.keys()].filter(p => p.startsWith('adapters/codex/skills/'));
388+
const codexPaths = [...files.keys()].filter(p => p.startsWith('adapters/codex/skills/') && p.endsWith('SKILL.md'));
386389
for (const codexPath of codexPaths) {
387390
const content = files.get(codexPath);
388391
// Should NOT contain literal CLAUDE_PLUGIN_ROOT or PLUGIN_ROOT variables
@@ -393,7 +396,7 @@ describe('gen-adapters', () => {
393396

394397
test('Codex skill files do not contain raw AskUserQuestion', () => {
395398
const { files } = genAdapters.computeAdapters();
396-
const codexPaths = [...files.keys()].filter(p => p.startsWith('adapters/codex/skills/'));
399+
const codexPaths = [...files.keys()].filter(p => p.startsWith('adapters/codex/skills/') && p.endsWith('SKILL.md'));
397400
for (const codexPath of codexPaths) {
398401
const content = files.get(codexPath);
399402
expect(content).not.toContain('AskUserQuestion');
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Slop Pattern Categories
2+
3+
Detailed reference for all slop patterns detected by the pipeline.
4+
5+
## Pattern Categories
6+
7+
### Console Debugging
8+
9+
| Language | Patterns | Severity |
10+
|----------|----------|----------|
11+
| JavaScript | `console.log()`, `console.debug()`, `console.info()` | medium |
12+
| Python | `print()`, `import pdb`, `breakpoint()` | medium |
13+
| Rust | `println!()`, `dbg!()`, `eprintln!()` | medium |
14+
15+
**Excludes**: Test files, CLI entry points, config files
16+
17+
### Unsafe Error Handling (Rust)
18+
19+
| Pattern | Severity | Better Alternatives |
20+
|---------|----------|---------------------|
21+
| `.unwrap()` | medium | `.expect("msg")`, `.unwrap_or(default)`, `?` operator |
22+
23+
Bare `.unwrap()` calls can cause panics in production. Prefer:
24+
- `.expect("descriptive message")` - panic with context
25+
- `.unwrap_or(default)` / `.unwrap_or_default()` - provide fallback
26+
- `.unwrap_or_else(\|\| ...)` - lazy fallback computation
27+
- `?` operator - propagate errors to caller
28+
- `.ok()` / `.map()` / `.and_then()` - transform Result/Option
29+
30+
**Excludes**: Test files, examples, benchmarks
31+
32+
### Placeholder Code
33+
34+
| Pattern | Language | Severity |
35+
|---------|----------|----------|
36+
| `throw new Error("TODO: ...")` | JavaScript | high |
37+
| `todo!()`, `unimplemented!()` | Rust | high |
38+
| `raise NotImplementedError` | Python | high |
39+
| `panic("TODO: ...")` | Go | high |
40+
| Empty function bodies `{}` | All | high |
41+
| `pass` only functions | Python | high |
42+
43+
### Error Handling Issues
44+
45+
| Pattern | Description | Fix Strategy |
46+
|---------|-------------|--------------|
47+
| Empty catch blocks | `catch (e) {}` | add_logging |
48+
| Silent except | `except: pass` | add_logging |
49+
50+
### Hardcoded Secrets
51+
52+
**Critical severity** - always flagged for manual review.
53+
54+
| Pattern | Examples |
55+
|---------|----------|
56+
| Generic credentials | `password=`, `api_key=`, `secret=` |
57+
| JWT tokens | `eyJ...` base64 pattern |
58+
| Provider-specific | `sk-` (OpenAI), `ghp_` (GitHub), `AKIA` (AWS) |
59+
60+
**Excludes**: Template placeholders (`${VAR}`, `{{VAR}}`), masked values (`xxxxxx`)
61+
62+
### Documentation Issues
63+
64+
| Pattern | Description | Severity |
65+
|---------|-------------|----------|
66+
| JSDoc > 3x function | Excessive documentation | medium |
67+
| Issue/PR references | `// #123`, `// PR #456` | medium |
68+
| Stale file references | `// see auth-flow.md` | low |
69+
70+
### Code Smells
71+
72+
| Pattern | Description | Severity |
73+
|---------|-------------|----------|
74+
| Boolean blindness | `fn(true, false, true)` | medium |
75+
| Message chains | `a.b().c().d().e()` | low |
76+
| Mutable globals | `let CONSTANT = ...` | high |
77+
| Dead code | Unreachable after return | high |
78+
79+
### Verbosity Patterns
80+
81+
| Pattern | Examples | Severity |
82+
|---------|----------|----------|
83+
| AI preambles | "Certainly!", "I'd be happy to help" | low |
84+
| Marketing buzzwords | "synergize", "paradigm shift" | low |
85+
| Hedging language | "it's worth noting", "arguably" | low |
86+
87+
## Certainty Levels
88+
89+
### HIGH Certainty
90+
- Direct regex match
91+
- Definitive slop pattern
92+
- Safe for auto-fix
93+
94+
### MEDIUM Certainty
95+
- Multi-pass analysis required
96+
- Review context before fixing
97+
- May need human judgment
98+
99+
### LOW Certainty
100+
- Heuristic detection
101+
- High false positive rate
102+
- Flag only, no auto-fix
103+
104+
## Auto-Fix Strategies
105+
106+
| Strategy | When Used |
107+
|----------|-----------|
108+
| `remove` | Debug statements, trailing whitespace |
109+
| `replace` | Mixed indentation, multiple blank lines |
110+
| `add_logging` | Empty error handlers |
111+
| `flag` | Secrets, placeholders, code smells |
112+
113+
## Multi-Pass Analyzers
114+
115+
These patterns require structural analysis beyond regex:
116+
117+
- `doc_code_ratio_js` - JSDoc/function ratio
118+
- `over_engineering_metrics` - File/export ratios
119+
- `buzzword_inflation` - Claims vs evidence
120+
- `infrastructure_without_implementation` - Setup without usage
121+
- `dead_code` - Unreachable code detection
122+
- `shotgun_surgery` - Git co-change analysis
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Slop Detection CLI
4+
* Runs the detection pipeline and outputs structured findings
5+
*
6+
* Usage: node detect.js [path] [--apply] [--deep] [--compact]
7+
*/
8+
9+
const path = require('path');
10+
const fs = require('fs');
11+
12+
// Resolve lib relative to script location, falling back to the installed
13+
// plugin copy (~/.agentsys) when the script is shipped without lib/ beside it
14+
const libCandidates = [
15+
path.join(__dirname, '..', 'lib'),
16+
path.join(process.env.AGENTSYS_DIR || path.join(require('os').homedir(), '.agentsys'), 'plugins', 'deslop', 'lib')
17+
];
18+
const libPath = libCandidates.find(p => fs.existsSync(p)) || libCandidates[0];
19+
const { runPipeline } = require(path.join(libPath, 'patterns', 'pipeline'));
20+
21+
function parseArgs(args) {
22+
const options = {
23+
path: '.',
24+
mode: 'report',
25+
thoroughness: 'normal',
26+
compact: false,
27+
maxFindings: 10
28+
};
29+
30+
for (let i = 0; i < args.length; i++) {
31+
const arg = args[i];
32+
if (arg === '--apply') {
33+
options.mode = 'apply';
34+
} else if (arg === '--deep') {
35+
options.thoroughness = 'deep';
36+
} else if (arg === '--quick') {
37+
options.thoroughness = 'quick';
38+
} else if (arg === '--compact') {
39+
options.compact = true;
40+
} else if (arg === '--max' && args[i + 1]) {
41+
options.maxFindings = parseInt(args[++i], 10);
42+
} else if (!arg.startsWith('-')) {
43+
options.path = arg;
44+
}
45+
}
46+
47+
return options;
48+
}
49+
50+
function formatFindings(result, compact, maxFindings = 10) {
51+
// Handle pipeline output format (has 'findings' array or 'summary' object)
52+
const findings = result.findings || [];
53+
const summary = result.summary || {};
54+
55+
if (compact) {
56+
// Compact table format for token efficiency
57+
console.log('\n## Slop Detection Results\n');
58+
console.log('| File | Line | Pattern | Severity | Certainty |');
59+
console.log('|------|------|---------|----------|-----------|');
60+
61+
for (const finding of findings.slice(0, maxFindings)) {
62+
const file = (finding.file || finding.path || '').length > 30
63+
? '...' + (finding.file || finding.path || '').slice(-27)
64+
: (finding.file || finding.path || '');
65+
const line = finding.line || finding.lineNumber || '-';
66+
const pattern = finding.patternName || finding.pattern || finding.type || 'unknown';
67+
const severity = finding.severity || 'medium';
68+
const certainty = finding.certainty || 'MEDIUM';
69+
console.log(`| ${file} | ${line} | ${pattern} | ${severity} | ${certainty} |`);
70+
}
71+
72+
const total = summary.totalFindings || findings.length;
73+
const bySeverity = summary.bySeverity || {};
74+
console.log(`\n**Total**: ${total} findings`);
75+
console.log(`**By Severity**: critical=${bySeverity.critical || 0}, high=${bySeverity.high || 0}, medium=${bySeverity.medium || 0}, low=${bySeverity.low || 0}`);
76+
} else {
77+
// Full JSON output
78+
console.log(JSON.stringify(result, null, 2));
79+
}
80+
}
81+
82+
async function main() {
83+
const args = process.argv.slice(2);
84+
85+
// Help
86+
if (args.includes('--help') || args.includes('-h')) {
87+
console.log(`
88+
Slop Detection CLI
89+
90+
Usage: node detect.js [path] [options]
91+
92+
Options:
93+
--apply Apply auto-fixes (default: report only)
94+
--deep Deep analysis with all analyzers
95+
--quick Quick regex-only scan
96+
--compact Output as markdown table (token efficient)
97+
--max N Maximum findings to return (default: 10)
98+
--help Show this help
99+
100+
Examples:
101+
node detect.js # Scan current directory
102+
node detect.js src/ # Scan src/ directory
103+
node detect.js --apply --compact # Fix and show compact results
104+
`);
105+
process.exit(0);
106+
}
107+
108+
const options = parseArgs(args);
109+
110+
// Validate path exists
111+
if (!fs.existsSync(options.path)) {
112+
console.error(`Error: Path not found: ${options.path}`);
113+
process.exit(1);
114+
}
115+
116+
try {
117+
// runPipeline takes (repoPath, options) - async function
118+
const result = await runPipeline(options.path, {
119+
mode: options.mode,
120+
thoroughness: options.thoroughness
121+
});
122+
123+
formatFindings(result, options.compact, options.maxFindings);
124+
125+
// Exit with error code if critical findings
126+
const bySeverity = result.summary?.bySeverity || {};
127+
if (bySeverity.critical > 0) {
128+
process.exit(2);
129+
}
130+
} catch (error) {
131+
console.error(`Error running detection: ${error.message}`);
132+
process.exit(1);
133+
}
134+
}
135+
136+
main().catch(err => {
137+
console.error(`Fatal error: ${err.message}`);
138+
process.exit(1);
139+
});

plugins/deslop/scripts/detect.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@
99
const path = require('path');
1010
const fs = require('fs');
1111

12-
// Resolve lib relative to script location (works with ${CLAUDE_PLUGIN_ROOT})
13-
const libPath = path.join(__dirname, '..', 'lib');
12+
// Resolve lib relative to script location, falling back to the installed
13+
// plugin copy (~/.agentsys) when the script is shipped without lib/ beside it
14+
const libCandidates = [
15+
path.join(__dirname, '..', 'lib'),
16+
path.join(process.env.AGENTSYS_DIR || path.join(require('os').homedir(), '.agentsys'), 'plugins', 'deslop', 'lib')
17+
];
18+
const libPath = libCandidates.find(p => fs.existsSync(p)) || libCandidates[0];
1419
const { runPipeline } = require(path.join(libPath, 'patterns', 'pipeline'));
1520

1621
function parseArgs(args) {

scripts/dev-install.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,18 @@ function installCodex() {
409409
});
410410

411411
fs.writeFileSync(destPath, content);
412+
413+
// Ship plugin-level references/ and scripts/ with the plugin's primary
414+
// skill (skillName === plugin) so skill-relative resource pointers in
415+
// the SKILL.md resolve from the installed skill directory.
416+
if (skillName === plugin) {
417+
for (const resDir of ['references', 'scripts']) {
418+
const srcResDir = path.join(SOURCE_DIR, 'plugins', plugin, resDir);
419+
if (fs.existsSync(srcResDir)) {
420+
fs.cpSync(srcResDir, path.join(skillDir, resDir), { recursive: true });
421+
}
422+
}
423+
}
412424
log(` [OK] ${skillName}`);
413425
}
414426
}

scripts/gen-adapters.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@ function normalizePath(p) {
3838
return p.replace(/\\/g, '/');
3939
}
4040

41+
// Recursively list files under a directory
42+
function walkFiles(dir) {
43+
const out = [];
44+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
45+
const abs = path.join(dir, entry.name);
46+
if (entry.isDirectory()) out.push(...walkFiles(abs));
47+
else if (entry.isFile()) out.push(abs);
48+
}
49+
return out;
50+
}
51+
4152
function computeAdapters() {
4253
discovery.invalidateCache();
4354

@@ -112,6 +123,22 @@ function computeAdapters() {
112123

113124
const relPath = normalizePath(path.join('adapters', 'codex', 'skills', skillName, 'SKILL.md'));
114125
files.set(relPath, content);
126+
127+
// Ship plugin-level references/ and scripts/ with the plugin's primary
128+
// codex skill (skillName === plugin) so skill-relative resource pointers
129+
// in the SKILL.md resolve after install (install.sh copies these
130+
// subdirectories into the target skill directory).
131+
if (skillName === plugin) {
132+
for (const resDir of ['references', 'scripts']) {
133+
const srcResDir = path.join(ROOT_DIR, 'plugins', plugin, resDir);
134+
if (!fs.existsSync(srcResDir)) continue;
135+
for (const resFile of walkFiles(srcResDir)) {
136+
const resRel = path.relative(srcResDir, resFile);
137+
const resPath = normalizePath(path.join('adapters', 'codex', 'skills', skillName, resDir, resRel));
138+
files.set(resPath, fs.readFileSync(resFile, 'utf8'));
139+
}
140+
}
141+
}
115142
}
116143

117144
if (emptyDescSkills.length > 0) {

0 commit comments

Comments
 (0)