diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index cd4bf1d5..11d1b639 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -1,13 +1,26 @@
{
"name": "awesome-slash",
- "description": "8 specialized plugins for AI workflow automation - task orchestration, PR workflow, slop detection, code review, drift detection, enhancement analysis, documentation sync, and repo mapping",
+ "description": "9 specialized plugins for AI workflow automation - task orchestration, PR workflow, slop detection, code review, drift detection, enhancement analysis, documentation sync, repo mapping, and perf investigations",
"version": "3.3.3",
"owner": {
"name": "Avi Fenesh",
"url": "https://github.com/avifenesh"
},
"repository": "https://github.com/avifenesh/awesome-slash",
- "keywords": ["ai", "llm", "agents", "agentic", "claude-code", "opencode", "codex", "mcp", "automation", "workflow", "code-review", "multi-agent"],
+ "keywords": [
+ "ai",
+ "llm",
+ "agents",
+ "agentic",
+ "claude-code",
+ "opencode",
+ "codex",
+ "mcp",
+ "automation",
+ "workflow",
+ "code-review",
+ "multi-agent"
+ ],
"plugins": [
{
"name": "next-task",
@@ -64,12 +77,29 @@
"description": "AST-based repository map generation using ast-grep with incremental updates for faster drift analysis",
"version": "3.3.3",
"category": "development"
+ },
+ {
+ "name": "perf",
+ "source": "./plugins/perf",
+ "description": "Rigorous performance investigation workflow with baselines, profiling, hypotheses, and evidence-backed decisions",
+ "version": "3.3.3",
+ "category": "development"
}
],
"mcpServer": {
"name": "awesome-slash",
"source": "./mcp-server",
"description": "Cross-platform MCP server with 3-phase slop detection pipeline and enhance analyzers",
- "tools": ["workflow_status", "workflow_start", "workflow_resume", "workflow_abort", "task_discover", "review_code", "slop_detect", "enhance_analyze", "repo_map"]
+ "tools": [
+ "workflow_status",
+ "workflow_start",
+ "workflow_resume",
+ "workflow_abort",
+ "task_discover",
+ "review_code",
+ "slop_detect",
+ "enhance_analyze",
+ "repo_map"
+ ]
}
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0dd515ca..8a2c0773 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,16 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
-## [3.3.3] - 2026-01-29
+### Added
+- **/enhance Hooks Analyzer** - New hook checks for frontmatter completeness and basic safety cues
+- **/enhance Skills Analyzer** - New SKILL.md checks for frontmatter and trigger phrase clarity
+- **Enhance MCP Tool** - `enhance_analyze` now supports `hooks` and `skills` focus targets
-### Fixed
-- **Windows Path Template Substitution** - Fixed plugin path template substitution across 18 command/agent files
- - Replaced `'${CLAUDE_PLUGIN_ROOT}'.replace()` with runtime `process.env` access
- - Pattern: `(process.env.CLAUDE_PLUGIN_ROOT || process.env.PLUGIN_ROOT || '').replace(/\/g, '/')`
- - Supports both CLAUDE_PLUGIN_ROOT and PLUGIN_ROOT for cross-platform compatibility
- - Added validation for missing environment variables in bash sections
- - Bash pattern: `${CLAUDE_PLUGIN_ROOT:-$PLUGIN_ROOT}` with explicit error handling
- - Fixes: Template strings in require() paths were not being substituted at runtime on Windows
+### Changed
+- **Enhance Orchestrator** - Expanded to run hooks/skills analyzers alongside existing enhancers
## [3.3.2] - 2026-01-29
diff --git a/README.md b/README.md
index d9d12128..2fdace80 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ AI models can write code. That's not the hard part anymore. The hard part is eve
| Section | What's there |
|---------|--------------|
-| [Commands](#commands) | All 8 commands with jump links |
+| [Commands](#commands) | All 9 commands with jump links |
| [What This Does](#what-this-project-does) | The problem and how this solves it |
| [What's Different](#what-makes-this-different) | Why this isn't just another AI tool |
| [Design Philosophy](#design-philosophy) | The thinking behind the architecture |
@@ -34,8 +34,9 @@ AI models can write code. That's not the hard part anymore. The hard part is eve
| [`/audit-project`](#audit-project) | Multi-agent code review until issues resolved | [→](#audit-project) |
| [`/drift-detect`](#drift-detect) | Compares your docs to actual code state | [→](#drift-detect) |
| [`/repo-map`](#repo-map) | Builds a cached AST repo map for fast analysis | [→](#repo-map) |
-| [`/enhance`](#enhance) | Analyzes prompts, plugins, docs for improvements | [→](#enhance) |
+| [`/enhance`](#enhance) | Analyzes prompts, plugins, agents, docs, hooks, skills | [→](#enhance) |
| [`/sync-docs`](#sync-docs) | Syncs documentation with code changes | [→](#sync-docs) |
+| [`/perf`](#perf) | Runs structured performance investigations | [→](#perf) |
---
@@ -455,9 +456,9 @@ Tools like `/drift-detect` and planners can use the map instead of re-scanning t
### /enhance
-**Purpose:** Analyzes your prompts, plugins, agents, and docs for improvement opportunities.
+**Purpose:** Analyzes your prompts, plugins, agents, docs, hooks, and skills for improvement opportunities.
-**Five analyzers run in parallel:**
+**Seven analyzers run in parallel:**
| Analyzer | What it checks |
|----------|----------------|
@@ -466,6 +467,8 @@ Tools like `/drift-detect` and planners can use the map instead of re-scanning t
| claudemd-enhancer | CLAUDE.md/AGENTS.md structure, token efficiency |
| docs-enhancer | Documentation readability, RAG optimization |
| prompt-enhancer | Prompt engineering patterns, clarity, examples |
+| hooks-enhancer | Hook frontmatter, structure, safety |
+| skills-enhancer | SKILL.md structure, trigger phrases |
**Each finding includes:**
- Certainty level (HIGH/MEDIUM/LOW)
@@ -484,6 +487,32 @@ Tools like `/drift-detect` and planners can use the map instead of re-scanning t
---
+### /perf
+
+**Purpose:** Run structured performance investigations with baselines, profiling, and evidence‑backed decisions.
+
+**Usage:**
+
+```bash
+/perf # Start new investigation
+/perf --resume # Resume previous investigation
+```
+
+**Phase flags (advanced):**
+
+```bash
+/perf --phase baseline --command "npm run bench" --version v1.2.0
+/perf --phase breaking-point --command "npm run bench" --param-min 1 --param-max 500
+/perf --phase constraints --command "npm run bench" --cpu 1 --memory 1GB
+/perf --phase hypotheses --hypotheses-file perf-hypotheses.json
+/perf --phase code-paths
+/perf --phase optimization --change "reduce allocations"
+/perf --phase decision --verdict stop --rationale "no measurable improvement"
+/perf --phase consolidation --version v1.2.0
+```
+
+---
+
### /sync-docs
**Purpose:** Sync documentation with actual code changes—find outdated refs, update CHANGELOG, flag stale examples.
diff --git a/__tests__/enhance-hooks-skills-analyzer.test.js b/__tests__/enhance-hooks-skills-analyzer.test.js
new file mode 100644
index 00000000..a6d84454
--- /dev/null
+++ b/__tests__/enhance-hooks-skills-analyzer.test.js
@@ -0,0 +1,64 @@
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const { analyzeHook, analyzeAllHooks } = require('../lib/enhance/hook-analyzer');
+const { analyzeSkill, analyzeAllSkills } = require('../lib/enhance/skill-analyzer');
+
+describe('enhance hook/skill analyzers', () => {
+ let tempDir;
+
+ beforeEach(() => {
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'enhance-test-'));
+ });
+
+ afterEach(() => {
+ fs.rmSync(tempDir, { recursive: true, force: true });
+ });
+
+ test('analyzeHook detects missing frontmatter', () => {
+ const hookPath = path.join(tempDir, 'hooks');
+ fs.mkdirSync(hookPath, { recursive: true });
+ const filePath = path.join(hookPath, 'pre-commit.md');
+ fs.writeFileSync(filePath, '# Hook\n\nNo frontmatter.');
+
+ const result = analyzeHook(filePath);
+ expect(result.structureIssues.length).toBeGreaterThanOrEqual(1);
+ expect(result.structureIssues.some(issue => /Missing YAML frontmatter/i.test(issue.issue))).toBe(true);
+ });
+
+ test('analyzeAllHooks scans nested hooks directories', () => {
+ const hookA = path.join(tempDir, 'plugins', 'alpha', 'hooks');
+ const hookB = path.join(tempDir, 'tools', 'hooks');
+ fs.mkdirSync(hookA, { recursive: true });
+ fs.mkdirSync(hookB, { recursive: true });
+ fs.writeFileSync(path.join(hookA, 'a.md'), '---\nname: a\ndescription: test\n---\n');
+ fs.writeFileSync(path.join(hookB, 'b.md'), '---\nname: b\ndescription: test\n---\n');
+
+ const results = analyzeAllHooks(tempDir);
+ expect(results.length).toBe(2);
+ });
+
+ test('analyzeSkill detects missing trigger phrase', () => {
+ const skillDir = path.join(tempDir, 'skills', 'example');
+ fs.mkdirSync(skillDir, { recursive: true });
+ const filePath = path.join(skillDir, 'SKILL.md');
+ fs.writeFileSync(filePath, '---\nname: example\ndescription: Helpful skill.\n---\n');
+
+ const result = analyzeSkill(filePath);
+ expect(result.triggerIssues.length).toBe(1);
+ expect(result.triggerIssues[0].issue).toMatch(/trigger phrase/i);
+ });
+
+ test('analyzeAllSkills finds nested SKILL.md files', () => {
+ const skillA = path.join(tempDir, 'skills', 'alpha');
+ const skillB = path.join(tempDir, 'plugins', 'beta', 'skills', 'beta-skill');
+ fs.mkdirSync(skillA, { recursive: true });
+ fs.mkdirSync(skillB, { recursive: true });
+ fs.writeFileSync(path.join(skillA, 'SKILL.md'), '---\nname: alpha\ndescription: Use when user asks about alpha.\n---\n');
+ fs.writeFileSync(path.join(skillB, 'SKILL.md'), '---\nname: beta\ndescription: Use when user asks about beta.\n---\n');
+
+ const results = analyzeAllSkills(tempDir);
+ expect(results.length).toBe(2);
+ });
+});
diff --git a/__tests__/perf-argument-parser.test.js b/__tests__/perf-argument-parser.test.js
new file mode 100644
index 00000000..440cd9b9
--- /dev/null
+++ b/__tests__/perf-argument-parser.test.js
@@ -0,0 +1,38 @@
+const { parseArguments } = require('../lib/perf/argument-parser');
+
+describe('perf argument parser', () => {
+ it('handles empty input', () => {
+ expect(parseArguments('')).toEqual([]);
+ expect(parseArguments(' ')).toEqual([]);
+ expect(parseArguments(null)).toEqual([]);
+ });
+
+ it('splits basic arguments', () => {
+ expect(parseArguments('--phase baseline --version v1')).toEqual([
+ '--phase',
+ 'baseline',
+ '--version',
+ 'v1'
+ ]);
+ });
+
+ it('preserves quoted values', () => {
+ const raw = '--command \"npm run bench -- --scenario small\" --quote \"latency spikes\"';
+ expect(parseArguments(raw)).toEqual([
+ '--command',
+ 'npm run bench -- --scenario small',
+ '--quote',
+ 'latency spikes'
+ ]);
+ });
+
+ it('supports single quotes', () => {
+ const raw = "--quote 'cache miss surge' --phase profiling";
+ expect(parseArguments(raw)).toEqual([
+ '--quote',
+ 'cache miss surge',
+ '--phase',
+ 'profiling'
+ ]);
+ });
+});
diff --git a/__tests__/perf-baseline.test.js b/__tests__/perf-baseline.test.js
new file mode 100644
index 00000000..b9de7f2d
--- /dev/null
+++ b/__tests__/perf-baseline.test.js
@@ -0,0 +1,42 @@
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const stateDir = require('../lib/platform/state-dir');
+const baselineStore = require('../lib/perf/baseline-store');
+const baselineComparator = require('../lib/perf/baseline-comparator');
+
+describe('perf baseline store', () => {
+ let tempDir;
+
+ beforeEach(() => {
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'perf-baseline-'));
+ process.env.AI_STATE_DIR = '.ai-state';
+ stateDir.clearCache();
+ });
+
+ afterEach(() => {
+ stateDir.clearCache();
+ delete process.env.AI_STATE_DIR;
+ fs.rmSync(tempDir, { recursive: true, force: true });
+ });
+
+ it('writes and reads baselines', () => {
+ baselineStore.writeBaseline('v1.0.0', { metrics: { latency: 120 }, command: 'npm run bench' }, tempDir);
+ const baseline = baselineStore.readBaseline('v1.0.0', tempDir);
+ expect(baseline).not.toBeNull();
+ expect(baseline.metrics.latency).toBe(120);
+ });
+
+ it('rejects invalid baseline versions', () => {
+ expect(() => baselineStore.getBaselinePath('../v1.0.0', tempDir)).toThrow();
+ });
+
+ it('compares baseline metrics', () => {
+ const result = baselineComparator.compareBaselines(
+ { metrics: { latency: 100 } },
+ { metrics: { latency: 125 } }
+ );
+ expect(result.metrics.latency.delta).toBe(25);
+ });
+});
diff --git a/__tests__/perf-benchmark-runner.test.js b/__tests__/perf-benchmark-runner.test.js
new file mode 100644
index 00000000..5c491408
--- /dev/null
+++ b/__tests__/perf-benchmark-runner.test.js
@@ -0,0 +1,34 @@
+const { parseMetrics } = require('../lib/perf/benchmark-runner');
+
+describe('perf benchmark parser', () => {
+ it('parses single scenario metrics', () => {
+ const output = [
+ 'noise',
+ 'PERF_METRICS_START',
+ '{"latency_ms":120,"throughput_rps":450}',
+ 'PERF_METRICS_END',
+ 'tail'
+ ].join('\n');
+
+ const result = parseMetrics(output);
+ expect(result.ok).toBe(true);
+ expect(result.metrics.latency_ms).toBe(120);
+ });
+
+ it('parses multi-scenario metrics', () => {
+ const output = [
+ 'PERF_METRICS_START',
+ '{"scenarios":{"low":{"latency_ms":120},"high":{"latency_ms":450}}}',
+ 'PERF_METRICS_END'
+ ].join('\n');
+
+ const result = parseMetrics(output);
+ expect(result.ok).toBe(true);
+ expect(result.metrics.scenarios.low.latency_ms).toBe(120);
+ });
+
+ it('fails when markers are missing', () => {
+ const result = parseMetrics('no metrics here');
+ expect(result.ok).toBe(false);
+ });
+});
diff --git a/__tests__/perf-breaking-point-runner.test.js b/__tests__/perf-breaking-point-runner.test.js
new file mode 100644
index 00000000..32a821c5
--- /dev/null
+++ b/__tests__/perf-breaking-point-runner.test.js
@@ -0,0 +1,19 @@
+const { runBreakingPointSearch } = require('../lib/perf/breaking-point-runner');
+
+describe('perf breaking point runner', () => {
+ it('finds breaking point in synthetic range', async () => {
+ const originalEnv = process.env.PERF_PARAM_VALUE;
+
+ const result = await runBreakingPointSearch({
+ command: 'node -e "const v=parseInt(process.env.PERF_PARAM_VALUE||\'0\',10); if(v>=5){process.exit(1);} console.log(\'PERF_METRICS_START\\n{}\\nPERF_METRICS_END\');"',
+ paramEnv: 'PERF_PARAM_VALUE',
+ min: 1,
+ max: 8
+ });
+
+ process.env.PERF_PARAM_VALUE = originalEnv;
+
+ expect(result.attempts).toBeGreaterThan(0);
+ expect(result.breakingPoint).toBe(5);
+ });
+});
diff --git a/__tests__/perf-checkpoint.test.js b/__tests__/perf-checkpoint.test.js
new file mode 100644
index 00000000..aa160e34
--- /dev/null
+++ b/__tests__/perf-checkpoint.test.js
@@ -0,0 +1,51 @@
+const checkpoint = require('../lib/perf/checkpoint');
+
+describe('perf checkpoint', () => {
+ it('builds checkpoint message', () => {
+ const message = checkpoint.buildCheckpointMessage({
+ phase: 'baseline',
+ id: 'perf-123',
+ baselineVersion: 'v1.0.0',
+ deltaSummary: 'latency -8%'
+ });
+
+ expect(message).toBe('perf: phase baseline [perf-123] baseline=v1.0.0 delta=latency -8%');
+ });
+
+ it('handles no-op commits gracefully', () => {
+ const childProcess = require('child_process');
+ const execSpy = jest.spyOn(childProcess, 'execSync').mockImplementation(() => {
+ throw new Error('not a git repo');
+ });
+
+ const result = checkpoint.commitCheckpoint({
+ phase: 'baseline',
+ id: 'perf-123'
+ });
+
+ if (!result.ok) {
+ expect(['not a git repo', 'nothing to commit', 'duplicate checkpoint']).toContain(result.reason);
+ } else {
+ expect(result.message).toContain('perf: phase baseline');
+ }
+
+ execSpy.mockRestore();
+ });
+
+ it('detects duplicate checkpoint messages', () => {
+ jest.resetModules();
+ jest.doMock('child_process', () => ({
+ execSync: jest.fn(() => 'perf: phase baseline [perf-123] baseline=n/a delta=n/a\n'),
+ execFileSync: jest.fn()
+ }));
+
+ const freshCheckpoint = require('../lib/perf/checkpoint');
+ const message = freshCheckpoint.buildCheckpointMessage({
+ phase: 'baseline',
+ id: 'perf-123'
+ });
+
+ expect(freshCheckpoint.isDuplicateCheckpoint(message)).toBe(true);
+ jest.dontMock('child_process');
+ });
+});
diff --git a/__tests__/perf-code-paths.test.js b/__tests__/perf-code-paths.test.js
new file mode 100644
index 00000000..466761e1
--- /dev/null
+++ b/__tests__/perf-code-paths.test.js
@@ -0,0 +1,43 @@
+const { normalizeKeywords, collectCodePaths } = require('../lib/perf/code-paths');
+
+describe('perf code-paths', () => {
+ it('normalizes scenario keywords', () => {
+ expect(normalizeKeywords('Auth latency spikes during login')).toEqual([
+ 'auth',
+ 'latency',
+ 'spikes',
+ 'during',
+ 'login'
+ ]);
+ });
+
+ it('collects code paths from repo map', () => {
+ const map = {
+ files: {
+ 'src/auth/login.js': {
+ symbols: {
+ exports: [],
+ functions: [{ name: 'login' }],
+ classes: [],
+ types: [],
+ constants: []
+ }
+ },
+ 'src/cache/index.js': {
+ symbols: {
+ exports: [],
+ functions: [{ name: 'warmCache' }],
+ classes: [],
+ types: [],
+ constants: []
+ }
+ }
+ }
+ };
+
+ const result = collectCodePaths(map, 'Login latency regression', 5);
+ expect(result.keywords).toContain('login');
+ expect(result.paths.length).toBe(1);
+ expect(result.paths[0].file).toBe('src/auth/login.js');
+ });
+});
diff --git a/__tests__/perf-consolidation.test.js b/__tests__/perf-consolidation.test.js
new file mode 100644
index 00000000..54b7e408
--- /dev/null
+++ b/__tests__/perf-consolidation.test.js
@@ -0,0 +1,35 @@
+const consolidation = require('../lib/perf/consolidation');
+const stateDir = require('../lib/platform/state-dir');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+describe('perf consolidation', () => {
+ let tempDir;
+
+ beforeEach(() => {
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'perf-consolidation-'));
+ process.env.AI_STATE_DIR = '.ai-state';
+ stateDir.clearCache();
+ });
+
+ afterEach(() => {
+ stateDir.clearCache();
+ delete process.env.AI_STATE_DIR;
+ fs.rmSync(tempDir, { recursive: true, force: true });
+ });
+
+ it('writes a single baseline per version', () => {
+ const result = consolidation.consolidateBaseline({
+ version: 'v1.0.0',
+ baseline: {
+ command: 'npm run bench',
+ metrics: { latency_ms: 120 }
+ }
+ }, tempDir);
+
+ expect(result.version).toBe('v1.0.0');
+ const baselinePath = path.join(tempDir, '.ai-state', 'perf', 'baselines', 'v1.0.0.json');
+ expect(fs.existsSync(baselinePath)).toBe(true);
+ });
+});
diff --git a/__tests__/perf-constraint-runner.test.js b/__tests__/perf-constraint-runner.test.js
new file mode 100644
index 00000000..ee101d93
--- /dev/null
+++ b/__tests__/perf-constraint-runner.test.js
@@ -0,0 +1,15 @@
+const { runConstraintTest } = require('../lib/perf/constraint-runner');
+
+describe('perf constraint runner', () => {
+ it('returns baseline, constrained, and delta metrics', () => {
+ const command = 'node -e "console.log(\'PERF_METRICS_START\'); console.log(JSON.stringify({latency_ms:120})); console.log(\'PERF_METRICS_END\');"';
+ const result = runConstraintTest({
+ command,
+ constraints: { cpu: '1', memory: '1GB' }
+ });
+
+ expect(result.baseline.metrics.latency_ms).toBe(120);
+ expect(result.constrained.metrics.latency_ms).toBe(120);
+ expect(result.delta.metrics.latency_ms.delta).toBe(0);
+ });
+});
diff --git a/__tests__/perf-log-helpers.test.js b/__tests__/perf-log-helpers.test.js
new file mode 100644
index 00000000..99a7aa94
--- /dev/null
+++ b/__tests__/perf-log-helpers.test.js
@@ -0,0 +1,86 @@
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const investigationState = require('../lib/perf/investigation-state');
+const stateDir = require('../lib/platform/state-dir');
+
+describe('perf log helpers', () => {
+ let tempDir;
+
+ beforeEach(() => {
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'perf-logs-'));
+ process.env.AI_STATE_DIR = '.ai-state';
+ stateDir.clearCache();
+ });
+
+ afterEach(() => {
+ stateDir.clearCache();
+ delete process.env.AI_STATE_DIR;
+ fs.rmSync(tempDir, { recursive: true, force: true });
+ });
+
+ it('appends setup, breaking-point, constraints, hypotheses, code-paths, optimization logs', () => {
+ const state = investigationState.initializeInvestigation({
+ scenario: 'Test scenario'
+ }, tempDir);
+
+ investigationState.appendSetupLog({
+ id: state.id,
+ userQuote: 'Run perf setup.',
+ scenario: 'Test scenario',
+ command: 'npm run bench',
+ version: 'v1.0.0'
+ }, tempDir);
+
+ investigationState.appendBreakingPointLog({
+ id: state.id,
+ userQuote: 'Find breaking point.',
+ paramEnv: 'PERF_PARAM_VALUE',
+ min: 1,
+ max: 10,
+ breakingPoint: 6
+ }, tempDir);
+
+ investigationState.appendConstraintLog({
+ id: state.id,
+ userQuote: 'Test constraints.',
+ constraints: { cpu: '1', memory: '1GB' },
+ delta: { metrics: { latency_ms: { delta: 10 } } }
+ }, tempDir);
+
+ investigationState.appendHypothesesLog({
+ id: state.id,
+ userQuote: 'Generate hypotheses.',
+ hypotheses: [
+ { id: 'H1', hypothesis: 'N+1 queries', evidence: 'src/db.js', confidence: 'medium' }
+ ]
+ }, tempDir);
+
+ investigationState.appendCodePathsLog({
+ id: state.id,
+ userQuote: 'Map code paths.',
+ keywords: ['auth', 'session'],
+ paths: [
+ { file: 'src/auth/index.js', score: 2, symbols: ['login', 'logout'] }
+ ]
+ }, tempDir);
+
+ investigationState.appendOptimizationLog({
+ id: state.id,
+ userQuote: 'Try optimization.',
+ change: 'reduce allocations',
+ delta: { metrics: { latency_ms: { delta: -5 } } },
+ verdict: 'inconclusive'
+ }, tempDir);
+
+ const logPath = investigationState.getInvestigationLogPath(state.id, tempDir);
+ const contents = fs.readFileSync(logPath, 'utf8');
+ expect(contents).toContain('Setup -');
+ expect(contents).toContain('Breaking Point -');
+ expect(contents).toContain('Constraints -');
+ expect(contents).toContain('Hypotheses -');
+ expect(contents).toContain('Code Paths -');
+ expect(contents).toContain('Optimization -');
+ });
+});
diff --git a/__tests__/perf-optimization-runner.test.js b/__tests__/perf-optimization-runner.test.js
new file mode 100644
index 00000000..bd72c00d
--- /dev/null
+++ b/__tests__/perf-optimization-runner.test.js
@@ -0,0 +1,16 @@
+const { runOptimizationExperiment } = require('../lib/perf/optimization-runner');
+
+describe('perf optimization runner', () => {
+ it('runs experiment and returns delta', () => {
+ const command = 'node -e "console.log(\'PERF_METRICS_START\'); console.log(JSON.stringify({latency_ms:120})); console.log(\'PERF_METRICS_END\');"';
+ const result = runOptimizationExperiment({
+ command,
+ changeSummary: 'noop change',
+ requireClean: false
+ });
+
+ expect(result.baseline.metrics.latency_ms).toBe(120);
+ expect(result.experiment.metrics.latency_ms).toBe(120);
+ expect(result.delta.metrics.latency_ms.delta).toBe(0);
+ });
+});
diff --git a/__tests__/perf-profilers.test.js b/__tests__/perf-profilers.test.js
new file mode 100644
index 00000000..c4f2a42b
--- /dev/null
+++ b/__tests__/perf-profilers.test.js
@@ -0,0 +1,47 @@
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const profilers = require('../lib/perf/profilers');
+
+describe('perf profiler selection', () => {
+ let tempDir;
+
+ beforeEach(() => {
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'perf-profilers-'));
+ });
+
+ afterEach(() => {
+ fs.rmSync(tempDir, { recursive: true, force: true });
+ });
+
+ it('selects node profiler for package.json', () => {
+ fs.writeFileSync(path.join(tempDir, 'package.json'), '{}', 'utf8');
+ const profiler = profilers.selectProfiler(tempDir);
+ expect(profiler.id).toBe('node');
+ });
+
+ it('selects java profiler when pom.xml exists', () => {
+ fs.writeFileSync(path.join(tempDir, 'pom.xml'), '', 'utf8');
+ const profiler = profilers.selectProfiler(tempDir);
+ expect(profiler.id).toBe('jfr');
+ });
+
+ it('selects go profiler when go.mod exists', () => {
+ fs.writeFileSync(path.join(tempDir, 'go.mod'), 'module test', 'utf8');
+ const profiler = profilers.selectProfiler(tempDir);
+ expect(profiler.id).toBe('pprof');
+ });
+
+ it('selects python profiler when requirements.txt exists', () => {
+ fs.writeFileSync(path.join(tempDir, 'requirements.txt'), 'flask', 'utf8');
+ const profiler = profilers.selectProfiler(tempDir);
+ expect(profiler.id).toBe('cprofile');
+ });
+
+ it('selects rust profiler when Cargo.toml exists', () => {
+ fs.writeFileSync(path.join(tempDir, 'Cargo.toml'), '[package]', 'utf8');
+ const profiler = profilers.selectProfiler(tempDir);
+ expect(profiler.id).toBe('perf');
+ });
+});
diff --git a/__tests__/perf-profiling-runner.test.js b/__tests__/perf-profiling-runner.test.js
new file mode 100644
index 00000000..10f9bdab
--- /dev/null
+++ b/__tests__/perf-profiling-runner.test.js
@@ -0,0 +1,20 @@
+const profilingRunner = require('../lib/perf/profiling-runner');
+const profilers = require('../lib/perf/profilers');
+
+describe('perf profiling runner', () => {
+ it('runs selected profiler command', () => {
+ const originalSelect = profilers.selectProfiler;
+ profilers.selectProfiler = () => ({
+ id: 'fake',
+ buildCommand: () => 'node -e "console.log(\'ok\')"',
+ parseOutput: () => ({ tool: 'fake', hotspots: ['file:1'], artifacts: ['out.prof'] })
+ });
+
+ const result = profilingRunner.runProfiling();
+ profilers.selectProfiler = originalSelect;
+
+ expect(result.ok).toBe(true);
+ expect(result.result.tool).toBe('fake');
+ expect(result.result.artifacts[0]).toBe('out.prof');
+ });
+});
diff --git a/__tests__/perf-schemas.test.js b/__tests__/perf-schemas.test.js
new file mode 100644
index 00000000..35fb369c
--- /dev/null
+++ b/__tests__/perf-schemas.test.js
@@ -0,0 +1,50 @@
+const { validateBaseline, validateInvestigationState } = require('../lib/perf/schemas');
+
+describe('perf schemas', () => {
+ it('flags invalid baseline metrics', () => {
+ const result = validateBaseline({
+ version: 'v1.0.0',
+ recordedAt: new Date().toISOString(),
+ command: 'npm run bench',
+ metrics: { latency: 'slow' }
+ });
+ expect(result.ok).toBe(false);
+ expect(result.errors.join(' ')).toContain('metric latency');
+ });
+
+ it('accepts minimal valid baseline', () => {
+ const result = validateBaseline({
+ version: 'v1.0.0',
+ recordedAt: new Date().toISOString(),
+ command: 'npm run bench',
+ metrics: { latency: 120 }
+ });
+ expect(result.ok).toBe(true);
+ });
+
+ it('accepts multi-scenario baseline metrics', () => {
+ const result = validateBaseline({
+ version: 'v1.0.0',
+ recordedAt: new Date().toISOString(),
+ command: 'npm run bench',
+ metrics: {
+ scenarios: {
+ low: { latency_ms: 120 },
+ high: { latency_ms: 450 }
+ }
+ }
+ });
+ expect(result.ok).toBe(true);
+ });
+
+ it('flags invalid investigation state', () => {
+ const result = validateInvestigationState({
+ schemaVersion: 1,
+ id: '',
+ status: 'in_progress',
+ phase: '',
+ scenario: {}
+ });
+ expect(result.ok).toBe(false);
+ });
+});
diff --git a/__tests__/perf-state.test.js b/__tests__/perf-state.test.js
new file mode 100644
index 00000000..9f78e48b
--- /dev/null
+++ b/__tests__/perf-state.test.js
@@ -0,0 +1,109 @@
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const stateDir = require('../lib/platform/state-dir');
+const investigationState = require('../lib/perf/investigation-state');
+
+describe('perf investigation state', () => {
+ let tempDir;
+
+ beforeEach(() => {
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'perf-state-'));
+ process.env.AI_STATE_DIR = '.ai-state';
+ stateDir.clearCache();
+ });
+
+ afterEach(() => {
+ stateDir.clearCache();
+ delete process.env.AI_STATE_DIR;
+ fs.rmSync(tempDir, { recursive: true, force: true });
+ });
+
+ it('creates and reads investigation state', () => {
+ const state = investigationState.initializeInvestigation({
+ scenario: 'API latency spike'
+ }, tempDir);
+
+ const readBack = investigationState.readInvestigation(tempDir);
+ expect(readBack).not.toBeNull();
+ expect(readBack.id).toBe(state.id);
+ expect(readBack.scenario.description).toBe('API latency spike');
+ });
+
+ it('appends investigation log entries', () => {
+ const state = investigationState.initializeInvestigation({}, tempDir);
+ investigationState.appendInvestigationLog(state.id, 'Entry 1', tempDir);
+ investigationState.appendInvestigationLog(state.id, 'Entry 2', tempDir);
+
+ const logPath = investigationState.getInvestigationLogPath(state.id, tempDir);
+ const contents = fs.readFileSync(logPath, 'utf8');
+ expect(contents).toContain('Entry 1');
+ expect(contents).toContain('Entry 2');
+ });
+
+ it('rejects invalid investigation ids', () => {
+ expect(() => investigationState.getInvestigationLogPath('../bad-id', tempDir)).toThrow();
+ });
+
+ it('appends baseline log entries', () => {
+ const state = investigationState.initializeInvestigation({
+ scenarios: [
+ { name: 'low', params: { concurrency: 10 } },
+ { name: 'high', params: { concurrency: 200 } }
+ ]
+ }, tempDir);
+ const baselinePath = path.join(tempDir, '.ai-state', 'perf', 'baselines', 'v1.0.0.json');
+
+ investigationState.appendBaselineLog({
+ id: state.id,
+ userQuote: 'Baseline the API latency.',
+ command: 'npm run bench',
+ metrics: { latency_ms: 120 },
+ baselinePath,
+ scenarios: state.scenario.scenarios
+ }, tempDir);
+
+ const logPath = investigationState.getInvestigationLogPath(state.id, tempDir);
+ const contents = fs.readFileSync(logPath, 'utf8');
+ expect(contents).toContain('Baseline -');
+ expect(contents).toContain('Baseline the API latency.');
+ expect(contents).toContain('npm run bench');
+ expect(contents).toContain('latency_ms');
+ expect(contents).toContain(baselinePath);
+ });
+
+ it('appends profiling log entries', () => {
+ const state = investigationState.initializeInvestigation({}, tempDir);
+ investigationState.appendProfilingLog({
+ id: state.id,
+ userQuote: 'Profile the hot path.',
+ tool: 'jfr',
+ command: 'java -XX:StartFlightRecording=duration=60s,filename=profile.jfr',
+ artifacts: ['profile.jfr'],
+ hotspots: ['src/App.java:42']
+ }, tempDir);
+
+ const logPath = investigationState.getInvestigationLogPath(state.id, tempDir);
+ const contents = fs.readFileSync(logPath, 'utf8');
+ expect(contents).toContain('Profiling -');
+ expect(contents).toContain('jfr');
+ expect(contents).toContain('profile.jfr');
+ });
+
+ it('appends decision log entries', () => {
+ const state = investigationState.initializeInvestigation({}, tempDir);
+ investigationState.appendDecisionLog({
+ id: state.id,
+ userQuote: 'Stop if improvement is negligible.',
+ verdict: 'stop',
+ rationale: 'No measurable improvement after 3 experiments.'
+ }, tempDir);
+
+ const logPath = investigationState.getInvestigationLogPath(state.id, tempDir);
+ const contents = fs.readFileSync(logPath, 'utf8');
+ expect(contents).toContain('Decision -');
+ expect(contents).toContain('Verdict: stop');
+ expect(contents).toContain('No measurable improvement');
+ });
+});
diff --git a/adapters/opencode-plugin/index.ts b/adapters/opencode-plugin/index.ts
index 5d015e15..bc3fb5ae 100644
--- a/adapters/opencode-plugin/index.ts
+++ b/adapters/opencode-plugin/index.ts
@@ -45,6 +45,8 @@ const AGENT_THINKING_CONFIG: Record **MCP is optional.** If you're running awesome-slash as native plugins/skills (Claude Code, OpenCode, Codex CLI) or invoking scripts directly, you can skip MCP. Use MCP when you want a generic tool endpoint for external clients.
+## Command Arguments ($ARGUMENTS)
+
+Claude Code passes a raw `$ARGUMENTS` string into commands. Commands should parse the raw string locally (including quoted values) to match Claude Code behavior.
+
+For OpenCode and Codex CLI, the installer adapts the platform argument handling to preserve `$ARGUMENTS` as a raw string. This keeps parsing consistent across platforms without changing command content.
+
## Claude Code (Native)
### Option 1: Marketplace (Recommended)
@@ -94,7 +100,7 @@ claude --plugin-dir /path/to/awesome-slash/plugins/next-task
| ci-fixer | sonnet | Fix CI failures and PR comments |
| simple-fixer | haiku | Execute pre-defined fixes |
-**enhance: Quality Analyzers (7 agents)**
+**enhance: Quality Analyzers (9 agents)**
| Agent | Model | Purpose |
|-------|-------|---------|
@@ -104,6 +110,8 @@ claude --plugin-dir /path/to/awesome-slash/plugins/next-task
| docs-enhancer | opus | Documentation quality |
| claudemd-enhancer | opus | Project memory optimization |
| prompt-enhancer | opus | General prompt quality |
+| hooks-enhancer | sonnet | Hook frontmatter and safety |
+| skills-enhancer | sonnet | SKILL.md structure and triggers |
| enhancement-reporter | sonnet | Format unified reports |
**drift-detect: Drift Detection (1 agent)**
@@ -286,7 +294,7 @@ When using the MCP server integration, these tools become available:
| `task_discover` | Find and prioritize tasks from gh-issues, linear, or tasks-md |
| `review_code` | Run pattern-based code review on changed files |
| `slop_detect` | Detect AI slop with certainty levels (HIGH/MEDIUM/LOW) |
-| `enhance_analyze` | Analyze plugins, agents, docs, prompts for improvements |
+| `enhance_analyze` | Analyze plugins, agents, docs, prompts, hooks, skills |
| `repo_map` | Generate or update cached AST repo map |
## Shared Libraries
diff --git a/docs/README.md b/docs/README.md
index 1c9b19b2..2e01131a 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -14,6 +14,7 @@ AI models can write code. The bottleneck is everything else—picking tasks, man
| See examples and workflows | [USAGE.md](./USAGE.md) |
| Understand how /next-task works | [workflows/NEXT-TASK.md](./workflows/NEXT-TASK.md) |
| Understand how /ship works | [workflows/SHIP.md](./workflows/SHIP.md) |
+| Run /perf investigations | [perf-requirements.md](./perf-requirements.md) |
| Use with OpenCode or Codex | [CROSS_PLATFORM.md](./CROSS_PLATFORM.md) |
| See all slop patterns | [reference/SLOP-PATTERNS.md](./reference/SLOP-PATTERNS.md) |
| See all agents | [reference/AGENTS.md](./reference/AGENTS.md) |
@@ -37,12 +38,14 @@ AI models can write code. The bottleneck is everything else—picking tasks, man
|----------|-------------|
| [workflows/NEXT-TASK.md](./workflows/NEXT-TASK.md) | Complete /next-task flow: phases, agents, state management, resume. |
| [workflows/SHIP.md](./workflows/SHIP.md) | Complete /ship flow: CI monitoring, review handling, merge, deploy. |
+| [perf-requirements.md](./perf-requirements.md) | /perf rules and required phases. |
+| [perf-research-methodology.md](./perf-research-methodology.md) | /perf process details, benchmarking method. |
### Reference
| Document | Description |
|----------|-------------|
-| [reference/AGENTS.md](./reference/AGENTS.md) | All 31 agents: purpose, model, tools, restrictions. |
+| [reference/AGENTS.md](./reference/AGENTS.md) | All 39 agents: purpose, model, tools, restrictions. |
| [reference/SLOP-PATTERNS.md](./reference/SLOP-PATTERNS.md) | All detection patterns by language, severity, auto-fix. |
| [reference/MCP-TOOLS.md](./reference/MCP-TOOLS.md) | MCP server tools: parameters, returns, platform config. |
@@ -67,7 +70,8 @@ AI models can write code. The bottleneck is everything else—picking tasks, man
| `/audit-project` | Multi-agent code review |
| `/drift-detect` | Compare docs to actual code |
| `/repo-map` | Build cached AST repo map |
-| `/enhance` | Analyze prompts, plugins, docs |
+| `/perf` | Performance investigation workflow |
+| `/enhance` | Analyze prompts, plugins, agents, docs, hooks, skills |
| `/sync-docs` | Sync docs with code changes |
### Internal Skills
diff --git a/docs/USAGE.md b/docs/USAGE.md
index 197040a5..2aea58d7 100644
--- a/docs/USAGE.md
+++ b/docs/USAGE.md
@@ -29,8 +29,9 @@ You shouldn't have to repeat the same requests every session. These commands han
| `/audit-project` | Multi-agent code review | Thorough analysis |
| `/drift-detect` | Compare docs to actual code | Plan drift detection |
| `/repo-map` | Build cached AST repo map | Faster analysis & symbol lookup |
-| `/enhance` | Analyze prompts, plugins, docs | Quality improvement |
+| `/enhance` | Analyze prompts, plugins, agents, docs, hooks, skills | Quality improvement |
| `/sync-docs` | Sync docs with code changes | Documentation sync |
+| `/perf` | Performance investigation workflow | Baselines, profiling, evidence |
---
@@ -190,6 +191,30 @@ Sync documentation with actual code state. Find outdated references, update CHAN
---
+### `/perf`
+
+Structured performance investigation with baselines, profiling, and evidence‑backed decisions.
+
+```bash
+/perf # Start new investigation
+/perf --resume # Resume previous investigation
+```
+
+**Phase flags (advanced):**
+
+```bash
+/perf --phase baseline --command "npm run bench" --version v1.2.0
+/perf --phase breaking-point --command "npm run bench" --param-min 1 --param-max 500
+/perf --phase constraints --command "npm run bench" --cpu 1 --memory 1GB
+/perf --phase hypotheses --hypotheses-file perf-hypotheses.json
+/perf --phase code-paths
+/perf --phase optimization --change "reduce allocations"
+/perf --phase decision --verdict stop --rationale "no measurable improvement"
+/perf --phase consolidation --version v1.2.0
+```
+
+---
+
### `/ship`
Complete PR workflow from commit to production.
diff --git a/docs/perf-requirements.md b/docs/perf-requirements.md
new file mode 100644
index 00000000..afc6cc71
--- /dev/null
+++ b/docs/perf-requirements.md
@@ -0,0 +1,61 @@
+# Performance Investigation Requirements
+
+This is the canonical contract for the /perf workflow. All agents, skills, hooks, and commands must follow these rules.
+
+## Non-Negotiable Rules
+
+1. Run benchmarks sequentially (never in parallel).
+2. Minimum run duration is 60s (30s only for binary search in breaking-point phase).
+3. Change one thing at a time; revert to baseline between experiments.
+4. Start narrow and expand only with explicit user approval.
+5. Verify anomalies by re-running.
+6. Establish a clean baseline before any experiment.
+7. Keep resource use minimal and repeatable.
+8. Check git history before hypotheses or changes.
+9. Clarify terminology before acting on ambiguous requests.
+10. Write logs + checkpoint commit after every phase.
+
+## Required Phases
+
+1. Setup and clarification
+2. Baseline establishment
+3. Breaking point discovery (binary search)
+4. Constraint testing (CPU/memory limits)
+5. Hypothesis generation
+6. Code-path analysis
+7. Profiling (CPU/memory/JFR/perf)
+8. Optimization experiments
+9. Decision point (continue/stop)
+10. Consolidation
+
+## Evidence Requirements
+
+Every phase log must include:
+- Exact user quote (verbatim)
+- Phase summary
+- Evidence pointers (commands, files, metrics)
+- Decision and rationale (when applicable)
+
+## Baseline Requirements
+
+- Baseline command must output PERF_METRICS markers.
+- Baseline JSON is stored at {state-dir}/perf/baselines/.json.
+- Baseline metrics must be numeric and comparable across runs.
+
+## State Requirements
+
+All perf state is stored under {state-dir}/perf/:
+- investigation.json
+- investigations/.md
+- baselines/.json
+
+State directory is platform-specific:
+- Claude Code: .claude/
+- OpenCode: .opencode/
+- Codex CLI: .codex/
+
+## Scope Boundaries
+
+- Only supported languages: Rust, Java, JavaScript, TypeScript, Go, Python.
+- Use repo-map and grep for code-path analysis before profiling.
+- Profiling artifacts must be captured and referenced in logs.
diff --git a/docs/perf-research-methodology.md b/docs/perf-research-methodology.md
new file mode 100644
index 00000000..b13f658e
--- /dev/null
+++ b/docs/perf-research-methodology.md
@@ -0,0 +1,85 @@
+# Performance Research Methodology
+
+This document defines how /perf investigations are executed. It complements perf-requirements.md with process detail.
+
+## 1. Setup
+
+- Confirm scenario, success criteria, and benchmark command.
+- Capture the user quote verbatim.
+- Record version label for the baseline.
+
+## 2. Baseline
+
+- Run the benchmark for at least 60s.
+- Require PERF_METRICS markers in output.
+- Parse metrics and store baseline JSON.
+- Re-run if results look anomalous.
+
+## 3. Breaking Point
+
+- Use binary search with 30s runs.
+- Parameterize via PERF_PARAM_VALUE (or configured env).
+- Record the smallest value that fails or degrades beyond thresholds.
+
+## 4. Constraints
+
+- Apply CPU/memory limits (default CPU=1, memory=1GB).
+- Measure delta vs baseline and log constraints + deltas.
+
+## 5. Hypotheses
+
+- Read recent git history and relevant code paths.
+- Produce up to 5 hypotheses with evidence and confidence.
+- No optimization changes in this phase.
+
+## 6. Code Paths
+
+- Use repo-map to identify entrypoints, handlers, and data access layers.
+- List top candidate files/symbols for profiling focus.
+- Record imports/exports when relevant to show wiring.
+
+## 7. Profiling
+
+- Prefer built-in tools for each language:
+ - Node: --cpu-prof
+ - Java: JFR
+ - Python: cProfile
+ - Go: pprof
+ - Rust: perf
+- Capture artifacts and hotspots; log file:line evidence.
+
+## 8. Optimization
+
+- One change per experiment.
+- Run 2+ validation passes per change.
+- Revert to baseline before next change.
+
+## 9. Decision
+
+- If improvement is not measurable, recommend stop.
+- If improvement exists, document next changes to pursue.
+
+## 10. Consolidation
+
+- Consolidate final baseline and log evidence.
+- Mark investigation complete.
+
+## Benchmarks Output Format
+
+Benchmarks must output PERF_METRICS markers, e.g.:
+
+```
+PERF_METRICS latency_ms=120.5 throughput_rps=2400
+```
+
+Scenario-specific metrics can be emitted as:
+
+```
+PERF_METRICS scenario=checkout latency_ms=180.1
+```
+
+## Noise Handling
+
+- Re-run if deviation >5% without clear cause.
+- Log anomalies and retest before recording results.
+- Keep environment stable (no background tasks, same config).
diff --git a/docs/reference/AGENTS.md b/docs/reference/AGENTS.md
index f0bb4b11..21064af4 100644
--- a/docs/reference/AGENTS.md
+++ b/docs/reference/AGENTS.md
@@ -2,7 +2,7 @@
Complete reference for all agents in awesome-slash.
-**TL;DR:** 31 agents across 5 plugins. opus for reasoning, sonnet for patterns, haiku for execution. Each agent does one thing well.
+**TL;DR:** 39 agents across 6 plugins. opus for reasoning, sonnet for patterns, haiku for execution. Each agent does one thing well.
---
@@ -12,9 +12,10 @@ Complete reference for all agents in awesome-slash.
|--------|--------|---------|
| next-task | 12 | [task-discoverer](#task-discoverer), [worktree-manager](#worktree-manager), [exploration-agent](#exploration-agent), [planning-agent](#planning-agent), [implementation-agent](#implementation-agent), [deslop-work](#deslop-work), [test-coverage-checker](#test-coverage-checker), [delivery-validator](#delivery-validator), [docs-updater](#docs-updater), [simple-fixer](#simple-fixer), [ci-monitor](#ci-monitor), [ci-fixer](#ci-fixer) |
| audit-project | 10 | [code-quality-reviewer](#code-quality-reviewer), [security-expert](#security-expert), [performance-engineer](#performance-engineer), [test-quality-guardian](#test-quality-guardian), [architecture-reviewer](#architecture-reviewer), [database-specialist](#database-specialist), [api-designer](#api-designer), [frontend-specialist](#frontend-specialist), [backend-specialist](#backend-specialist), [devops-reviewer](#devops-reviewer) |
-| enhance | 7 | [enhancement-orchestrator](#enhancement-orchestrator), [plugin-enhancer](#plugin-enhancer), [agent-enhancer](#agent-enhancer), [claudemd-enhancer](#claudemd-enhancer), [docs-enhancer](#docs-enhancer), [prompt-enhancer](#prompt-enhancer), [enhancement-reporter](#enhancement-reporter) |
+| enhance | 9 | [enhancement-orchestrator](#enhancement-orchestrator), [plugin-enhancer](#plugin-enhancer), [agent-enhancer](#agent-enhancer), [claudemd-enhancer](#claudemd-enhancer), [docs-enhancer](#docs-enhancer), [prompt-enhancer](#prompt-enhancer), [hooks-enhancer](#hooks-enhancer), [skills-enhancer](#skills-enhancer), [enhancement-reporter](#enhancement-reporter) |
| drift-detect | 1 | [plan-synthesizer](#plan-synthesizer) |
| repo-map | 1 | [map-validator](#map-validator) |
+| perf | 6 | [perf-orchestrator](#perf-orchestrator), [perf-theory-gatherer](#perf-theory-gatherer), [perf-theory-tester](#perf-theory-tester), [perf-code-paths](#perf-code-paths), [perf-investigation-logger](#perf-investigation-logger), [perf-analyzer](#perf-analyzer) |
**Design principle:** Each agent has a single responsibility. Complex work is decomposed into specialized agents that do one thing extremely well, then orchestrated together.
@@ -26,7 +27,7 @@ Complete reference for all agents in awesome-slash.
## Overview
-awesome-slash uses 31 specialized agents across 5 plugins. Each agent is optimized for a specific task and assigned a model based on complexity:
+awesome-slash uses 39 specialized agents across 6 plugins. Each agent is optimized for a specific task and assigned a model based on complexity:
| Model | Use Case | Cost |
|-------|----------|------|
@@ -35,7 +36,7 @@ awesome-slash uses 31 specialized agents across 5 plugins. Each agent is optimiz
| haiku | Mechanical execution, no judgment | Low |
**Agent types:**
-- **File-based agents** (21) - Defined in `plugins/*/agents/*.md` with frontmatter
+- **File-based agents** (29) - Defined in `plugins/*/agents/*.md` with frontmatter
- **Role-based agents** (10) - Defined inline via Task tool with specialized prompts
---
@@ -419,6 +420,36 @@ awesome-slash uses 31 specialized agents across 5 plugins. Each agent is optimiz
---
+### hooks-enhancer
+
+**Model:** sonnet
+**Purpose:** Analyze hook definitions.
+
+**Checks:**
+- Frontmatter presence and structure
+- Required name/description fields
+- Basic formatting expectations
+
+**Tools available:**
+- Read, Glob, Grep
+
+---
+
+### skills-enhancer
+
+**Model:** sonnet
+**Purpose:** Analyze SKILL.md quality.
+
+**Checks:**
+- Frontmatter presence and structure
+- Required name/description fields
+- Trigger phrase clarity ("Use when user asks")
+
+**Tools available:**
+- Read, Glob, Grep
+
+---
+
### enhancement-reporter
**Model:** sonnet
@@ -479,6 +510,73 @@ awesome-slash uses 31 specialized agents across 5 plugins. Each agent is optimiz
---
+## perf Plugin Agents
+
+### perf-orchestrator
+
+**Model:** opus
+**Purpose:** Coordinate /perf investigations across all phases.
+
+**What it does:**
+1. Enforces perf rules and phase order
+2. Spawns theory, profiling, and logging helpers
+3. Ensures checkpoints + evidence after each phase
+
+**Tools available:**
+- Read, Write, Edit, Task, Bash(git:*), Bash(npm:*), Bash(cargo:*), Bash(go:*), Bash(pytest:*), Bash(mvn:*), Bash(gradle:*)
+
+---
+
+### perf-theory-gatherer
+
+**Model:** opus
+**Purpose:** Generate hypotheses based on git history and evidence.
+
+**Tools available:**
+- Read, Bash(git:*), Bash(npm:*), Bash(pnpm:*), Bash(yarn:*), Bash(cargo:*), Bash(go:*), Bash(pytest:*), Bash(python:*), Bash(mvn:*), Bash(gradle:*)
+
+---
+
+### perf-theory-tester
+
+**Model:** opus
+**Purpose:** Validate hypotheses with controlled experiments.
+
+**Tools available:**
+- Read, Write, Edit, Bash(git:*), Bash(npm:*), Bash(pnpm:*), Bash(yarn:*), Bash(cargo:*), Bash(go:*), Bash(pytest:*), Bash(python:*), Bash(mvn:*), Bash(gradle:*)
+
+---
+
+### perf-code-paths
+
+**Model:** sonnet
+**Purpose:** Map entrypoints and likely hot files before profiling.
+
+**Tools available:**
+- Read, Grep, Glob
+
+---
+
+### perf-investigation-logger
+
+**Model:** sonnet
+**Purpose:** Append structured investigation logs with evidence.
+
+**Tools available:**
+- Read, Write
+
+---
+
+### perf-analyzer
+
+**Model:** opus
+**Purpose:** Synthesize findings into evidence-backed recommendations.
+
+**Tools available:**
+- Read, Write
+
+---
+
## audit-project Plugin Agents
These are role-based agents invoked via Task tool with specialized prompts. They use the built-in review subagent type with domain-specific instructions.
diff --git a/docs/reference/MCP-TOOLS.md b/docs/reference/MCP-TOOLS.md
index 900413b8..4f6b81a8 100644
--- a/docs/reference/MCP-TOOLS.md
+++ b/docs/reference/MCP-TOOLS.md
@@ -254,14 +254,14 @@ Detect AI slop patterns with certainty-based findings.
### enhance_analyze
-Analyze plugins, agents, docs, or prompts for enhancement opportunities.
+Analyze plugins, agents, docs, prompts, hooks, or skills for enhancement opportunities.
**Parameters:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| path | string | No | Directory to analyze (default: current directory) |
-| focus | string | No | Which analyzer: `all`, `plugin`, `agent`, `docs`, `claudemd`, `prompt` |
+| focus | string | No | Which analyzer: `all`, `plugin`, `agent`, `docs`, `claudemd`, `claude-memory`, `prompt`, `hooks`, `skills` |
| mode | string | No | `report` (default) or `apply` HIGH certainty fixes |
| compact | boolean | No | Use compact output format (default: true) |
diff --git a/lib/enhance/hook-analyzer.js b/lib/enhance/hook-analyzer.js
new file mode 100644
index 00000000..2530e111
--- /dev/null
+++ b/lib/enhance/hook-analyzer.js
@@ -0,0 +1,135 @@
+/**
+ * Hook analyzer for /enhance.
+ */
+
+const fs = require('fs');
+const path = require('path');
+const { hookPatterns } = require('./hook-patterns');
+const { parseMarkdownFrontmatter } = require('./agent-analyzer');
+
+function analyzeHook(hookPath) {
+ const results = {
+ hookName: path.basename(hookPath, '.md'),
+ hookPath,
+ structureIssues: []
+ };
+
+ if (!fs.existsSync(hookPath)) {
+ results.structureIssues.push({
+ issue: 'File not found',
+ file: hookPath,
+ certainty: 'HIGH',
+ patternId: 'file_not_found'
+ });
+ return results;
+ }
+
+ let content = '';
+ try {
+ content = fs.readFileSync(hookPath, 'utf8');
+ } catch (err) {
+ results.structureIssues.push({
+ issue: `Failed to read file: ${err.message}`,
+ file: hookPath,
+ certainty: 'HIGH',
+ patternId: 'read_error'
+ });
+ return results;
+ }
+
+ const missingFm = hookPatterns.missing_frontmatter.check(content);
+ if (missingFm) {
+ results.structureIssues.push({
+ ...missingFm,
+ file: hookPath,
+ certainty: hookPatterns.missing_frontmatter.certainty,
+ patternId: hookPatterns.missing_frontmatter.id
+ });
+ }
+
+ const { frontmatter } = parseMarkdownFrontmatter(content);
+ const missingName = hookPatterns.missing_name.check(frontmatter);
+ if (missingName) {
+ results.structureIssues.push({
+ ...missingName,
+ file: hookPath,
+ certainty: hookPatterns.missing_name.certainty,
+ patternId: hookPatterns.missing_name.id
+ });
+ }
+
+ const missingDescription = hookPatterns.missing_description.check(frontmatter);
+ if (missingDescription) {
+ results.structureIssues.push({
+ ...missingDescription,
+ file: hookPath,
+ certainty: hookPatterns.missing_description.certainty,
+ patternId: hookPatterns.missing_description.id
+ });
+ }
+
+ return results;
+}
+
+function analyzeAllHooks(hooksDir) {
+ const results = [];
+ if (!fs.existsSync(hooksDir)) return results;
+
+ const hookFiles = [];
+ const skipDirs = new Set(['node_modules', '.git', 'dist', 'build', 'out', 'target']);
+
+ function walk(dir) {
+ let entries;
+ try {
+ entries = fs.readdirSync(dir, { withFileTypes: true });
+ } catch (err) {
+ return;
+ }
+
+ for (const entry of entries) {
+ const fullPath = path.join(dir, entry.name);
+ if (entry.isDirectory()) {
+ if (!skipDirs.has(entry.name)) {
+ walk(fullPath);
+ }
+ continue;
+ }
+
+ if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
+ const parts = fullPath.split(path.sep);
+ if (parts.includes('hooks')) {
+ hookFiles.push(fullPath);
+ }
+ }
+ }
+
+ walk(hooksDir);
+
+ for (const file of hookFiles) {
+ results.push(analyzeHook(file));
+ }
+
+ return results;
+}
+
+function analyze(options = {}) {
+ const {
+ hook,
+ hooksDir = 'plugins/enhance/hooks'
+ } = options;
+
+ if (hook) {
+ const hookPath = hook.endsWith('.md')
+ ? hook
+ : path.join(hooksDir, `${hook}.md`);
+ return analyzeHook(hookPath);
+ }
+
+ return analyzeAllHooks(hooksDir);
+}
+
+module.exports = {
+ analyzeHook,
+ analyzeAllHooks,
+ analyze
+};
diff --git a/lib/enhance/hook-patterns.js b/lib/enhance/hook-patterns.js
new file mode 100644
index 00000000..472c789b
--- /dev/null
+++ b/lib/enhance/hook-patterns.js
@@ -0,0 +1,40 @@
+/**
+ * Hook patterns for /enhance.
+ */
+
+const hookPatterns = {
+ missing_frontmatter: {
+ id: 'missing_frontmatter',
+ certainty: 'HIGH',
+ check(content) {
+ if (!content || !content.trim().startsWith('---')) {
+ return { issue: 'Missing YAML frontmatter in hook file' };
+ }
+ return null;
+ }
+ },
+ missing_name: {
+ id: 'missing_name',
+ certainty: 'HIGH',
+ check(frontmatter) {
+ if (!frontmatter || !frontmatter.name) {
+ return { issue: 'Missing name in hook frontmatter' };
+ }
+ return null;
+ }
+ },
+ missing_description: {
+ id: 'missing_description',
+ certainty: 'HIGH',
+ check(frontmatter) {
+ if (!frontmatter || !frontmatter.description) {
+ return { issue: 'Missing description in hook frontmatter' };
+ }
+ return null;
+ }
+ }
+};
+
+module.exports = {
+ hookPatterns
+};
diff --git a/lib/enhance/index.js b/lib/enhance/index.js
index 542e81fd..07539241 100644
--- a/lib/enhance/index.js
+++ b/lib/enhance/index.js
@@ -16,6 +16,8 @@ const projectmemoryAnalyzer = require('./projectmemory-analyzer');
const projectmemoryPatterns = require('./projectmemory-patterns');
const promptAnalyzer = require('./prompt-analyzer');
const promptPatterns = require('./prompt-patterns');
+const hookAnalyzer = require('./hook-analyzer');
+const skillAnalyzer = require('./skill-analyzer');
const reporter = require('./reporter');
const fixer = require('./fixer');
@@ -26,6 +28,8 @@ module.exports = {
docsAnalyzer,
projectmemoryAnalyzer,
promptAnalyzer,
+ hookAnalyzer,
+ skillAnalyzer,
// Pattern modules
pluginPatterns,
@@ -72,6 +76,16 @@ module.exports = {
promptApplyFixes: promptAnalyzer.applyFixes,
promptGenerateReport: promptAnalyzer.generateReport,
+ // Convenience exports - Hooks
+ analyzeHook: hookAnalyzer.analyzeHook,
+ analyzeAllHooks: hookAnalyzer.analyzeAllHooks,
+ hooksAnalyze: hookAnalyzer.analyze,
+
+ // Convenience exports - Skills
+ analyzeSkill: skillAnalyzer.analyzeSkill,
+ analyzeAllSkills: skillAnalyzer.analyzeAllSkills,
+ skillsAnalyze: skillAnalyzer.analyze,
+
// Convenience exports - Orchestrator
generateOrchestratorReport: reporter.generateOrchestratorReport,
deduplicateOrchestratorFindings: reporter.deduplicateOrchestratorFindings
diff --git a/lib/enhance/reporter.js b/lib/enhance/reporter.js
index 7016a1f8..77b727c6 100644
--- a/lib/enhance/reporter.js
+++ b/lib/enhance/reporter.js
@@ -1091,7 +1091,7 @@ function generateOrchestratorReport(aggregatedResults, options = {}) {
lines.push('| Enhancer | HIGH | MEDIUM | LOW | Auto-Fixable |');
lines.push('|----------|------|--------|-----|--------------|');
- const enhancerTypes = ['plugin', 'agent', 'claudemd', 'docs', 'prompt'];
+ const enhancerTypes = ['plugin', 'agent', 'claudemd', 'docs', 'prompt', 'hooks', 'skills'];
let totalHigh = 0, totalMedium = 0, totalLow = 0, totalAutoFix = 0;
for (const enhancer of enhancerTypes) {
diff --git a/lib/enhance/skill-analyzer.js b/lib/enhance/skill-analyzer.js
new file mode 100644
index 00000000..023ac494
--- /dev/null
+++ b/lib/enhance/skill-analyzer.js
@@ -0,0 +1,144 @@
+/**
+ * Skill analyzer for /enhance.
+ */
+
+const fs = require('fs');
+const path = require('path');
+const { skillPatterns } = require('./skill-patterns');
+const { parseMarkdownFrontmatter } = require('./agent-analyzer');
+
+function analyzeSkill(skillPath) {
+ const results = {
+ skillName: path.basename(path.dirname(skillPath)),
+ skillPath,
+ structureIssues: [],
+ triggerIssues: []
+ };
+
+ if (!fs.existsSync(skillPath)) {
+ results.structureIssues.push({
+ issue: 'File not found',
+ file: skillPath,
+ certainty: 'HIGH',
+ patternId: 'file_not_found'
+ });
+ return results;
+ }
+
+ let content = '';
+ try {
+ content = fs.readFileSync(skillPath, 'utf8');
+ } catch (err) {
+ results.structureIssues.push({
+ issue: `Failed to read file: ${err.message}`,
+ file: skillPath,
+ certainty: 'HIGH',
+ patternId: 'read_error'
+ });
+ return results;
+ }
+
+ const missingFm = skillPatterns.missing_frontmatter.check(content);
+ if (missingFm) {
+ results.structureIssues.push({
+ ...missingFm,
+ file: skillPath,
+ certainty: skillPatterns.missing_frontmatter.certainty,
+ patternId: skillPatterns.missing_frontmatter.id
+ });
+ }
+
+ const { frontmatter } = parseMarkdownFrontmatter(content);
+ const missingName = skillPatterns.missing_name.check(frontmatter);
+ if (missingName) {
+ results.structureIssues.push({
+ ...missingName,
+ file: skillPath,
+ certainty: skillPatterns.missing_name.certainty,
+ patternId: skillPatterns.missing_name.id
+ });
+ }
+
+ const missingDescription = skillPatterns.missing_description.check(frontmatter);
+ if (missingDescription) {
+ results.structureIssues.push({
+ ...missingDescription,
+ file: skillPath,
+ certainty: skillPatterns.missing_description.certainty,
+ patternId: skillPatterns.missing_description.id
+ });
+ }
+
+ const missingTrigger = skillPatterns.missing_trigger_phrase.check(frontmatter);
+ if (missingTrigger) {
+ results.triggerIssues.push({
+ ...missingTrigger,
+ file: skillPath,
+ certainty: skillPatterns.missing_trigger_phrase.certainty,
+ patternId: skillPatterns.missing_trigger_phrase.id
+ });
+ }
+
+ return results;
+}
+
+function analyzeAllSkills(skillsDir) {
+ const results = [];
+ if (!fs.existsSync(skillsDir)) return results;
+
+ const skillFiles = [];
+ const skipDirs = new Set(['node_modules', '.git', 'dist', 'build', 'out', 'target']);
+
+ function walk(dir) {
+ let entries;
+ try {
+ entries = fs.readdirSync(dir, { withFileTypes: true });
+ } catch (err) {
+ return;
+ }
+
+ for (const entry of entries) {
+ const fullPath = path.join(dir, entry.name);
+ if (entry.isDirectory()) {
+ if (!skipDirs.has(entry.name)) {
+ walk(fullPath);
+ }
+ continue;
+ }
+
+ if (entry.isFile() && entry.name === 'SKILL.md') {
+ skillFiles.push(fullPath);
+ }
+ }
+ }
+
+ walk(skillsDir);
+
+ for (const skillPath of skillFiles) {
+ results.push(analyzeSkill(skillPath));
+ }
+
+ return results;
+}
+
+function analyze(options = {}) {
+ const {
+ skill,
+ skillsDir = 'plugins/enhance/skills'
+ } = options;
+
+ if (skill) {
+ const skillPath = skill.endsWith('SKILL.md')
+ ? skill
+ : path.join(skillsDir, skill, 'SKILL.md');
+ return analyzeSkill(skillPath);
+ }
+
+ return analyzeAllSkills(skillsDir);
+}
+
+module.exports = {
+ analyzeSkill,
+ analyzeAllSkills,
+ analyze
+};
diff --git a/lib/enhance/skill-patterns.js b/lib/enhance/skill-patterns.js
new file mode 100644
index 00000000..50872c58
--- /dev/null
+++ b/lib/enhance/skill-patterns.js
@@ -0,0 +1,51 @@
+/**
+ * Skill patterns for /enhance.
+ */
+
+const skillPatterns = {
+ missing_frontmatter: {
+ id: 'missing_frontmatter',
+ certainty: 'HIGH',
+ check(content) {
+ if (!content || !content.trim().startsWith('---')) {
+ return { issue: 'Missing YAML frontmatter in SKILL.md' };
+ }
+ return null;
+ }
+ },
+ missing_name: {
+ id: 'missing_name',
+ certainty: 'HIGH',
+ check(frontmatter) {
+ if (!frontmatter || !frontmatter.name) {
+ return { issue: 'Missing name in SKILL.md frontmatter' };
+ }
+ return null;
+ }
+ },
+ missing_description: {
+ id: 'missing_description',
+ certainty: 'HIGH',
+ check(frontmatter) {
+ if (!frontmatter || !frontmatter.description) {
+ return { issue: 'Missing description in SKILL.md frontmatter' };
+ }
+ return null;
+ }
+ },
+ missing_trigger_phrase: {
+ id: 'missing_trigger_phrase',
+ certainty: 'MEDIUM',
+ check(frontmatter) {
+ if (!frontmatter || !frontmatter.description) return null;
+ if (!/use when user asks/i.test(frontmatter.description)) {
+ return { issue: 'Description missing "Use when user asks" trigger phrase' };
+ }
+ return null;
+ }
+ }
+};
+
+module.exports = {
+ skillPatterns
+};
diff --git a/lib/index.js b/lib/index.js
index 646eb350..07706b6c 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -26,6 +26,7 @@ const policyQuestions = require('./sources/policy-questions');
const crossPlatform = require('./cross-platform');
const enhance = require('./enhance');
const repoMap = require('./repo-map');
+const perf = require('./perf');
/**
* Platform detection and verification utilities
@@ -228,6 +229,7 @@ module.exports = {
xplat,
enhance,
repoMap,
+ perf,
// Direct module access for backward compatibility
detectPlatform,
diff --git a/lib/perf/analyzer/index.js b/lib/perf/analyzer/index.js
new file mode 100644
index 00000000..87fd5c4f
--- /dev/null
+++ b/lib/perf/analyzer/index.js
@@ -0,0 +1,22 @@
+/**
+ * Perf analysis helpers.
+ *
+ * @module lib/perf/analyzer
+ */
+
+/**
+ * Build a compact summary of perf findings.
+ * @param {object} input
+ * @returns {object}
+ */
+function summarize(input = {}) {
+ return {
+ summary: input.summary || '',
+ recommendations: input.recommendations || [],
+ risks: input.risks || []
+ };
+}
+
+module.exports = {
+ summarize
+};
diff --git a/lib/perf/argument-parser.js b/lib/perf/argument-parser.js
new file mode 100644
index 00000000..46b04d35
--- /dev/null
+++ b/lib/perf/argument-parser.js
@@ -0,0 +1,65 @@
+/**
+ * Argument parsing helper for /perf.
+ *
+ * @module lib/perf/argument-parser
+ */
+
+function parseArguments(raw) {
+ if (!raw || typeof raw !== 'string') return [];
+
+ const args = [];
+ let current = '';
+ let quote = null;
+ let escaped = false;
+
+ for (let i = 0; i < raw.length; i++) {
+ const ch = raw[i];
+
+ if (escaped) {
+ current += ch;
+ escaped = false;
+ continue;
+ }
+
+ if (ch === '\\') {
+ if (quote) {
+ escaped = true;
+ continue;
+ }
+ }
+
+ if (quote) {
+ if (ch === quote) {
+ quote = null;
+ } else {
+ current += ch;
+ }
+ continue;
+ }
+
+ if (ch === '"' || ch === "'") {
+ quote = ch;
+ continue;
+ }
+
+ if (/\s/.test(ch)) {
+ if (current) {
+ args.push(current);
+ current = '';
+ }
+ continue;
+ }
+
+ current += ch;
+ }
+
+ if (current) {
+ args.push(current);
+ }
+
+ return args;
+}
+
+module.exports = {
+ parseArguments
+};
diff --git a/lib/perf/baseline-comparator.js b/lib/perf/baseline-comparator.js
new file mode 100644
index 00000000..7e71220a
--- /dev/null
+++ b/lib/perf/baseline-comparator.js
@@ -0,0 +1,50 @@
+/**
+ * Baseline comparison helpers
+ *
+ * @module lib/perf/baseline-comparator
+ */
+
+/**
+ * Compute delta between baseline and current metrics.
+ * Supports flat numeric values under baseline.metrics/current.metrics.
+ *
+ * @param {object} baseline
+ * @param {object} current
+ * @returns {object}
+ */
+function compareBaselines(baseline, current) {
+ const baselineMetrics = baseline?.metrics || {};
+ const currentMetrics = current?.metrics || {};
+ const keys = new Set([
+ ...Object.keys(baselineMetrics),
+ ...Object.keys(currentMetrics)
+ ]);
+
+ const deltas = {};
+ for (const key of keys) {
+ const baseValue = baselineMetrics[key];
+ const currentValue = currentMetrics[key];
+
+ if (typeof baseValue === 'number' && typeof currentValue === 'number') {
+ const delta = currentValue - baseValue;
+ const percent = baseValue === 0 ? null : delta / baseValue;
+ deltas[key] = { baseline: baseValue, current: currentValue, delta, percent };
+ } else {
+ deltas[key] = {
+ baseline: baseValue ?? null,
+ current: currentValue ?? null,
+ delta: null,
+ percent: null
+ };
+ }
+ }
+
+ return {
+ comparedAt: new Date().toISOString(),
+ metrics: deltas
+ };
+}
+
+module.exports = {
+ compareBaselines
+};
diff --git a/lib/perf/baseline-store.js b/lib/perf/baseline-store.js
new file mode 100644
index 00000000..f8c8a21f
--- /dev/null
+++ b/lib/perf/baseline-store.js
@@ -0,0 +1,127 @@
+/**
+ * Baseline storage utilities for /perf
+ *
+ * Stores baselines under:
+ * - {state-dir}/perf/baselines/{version}.json
+ *
+ * @module lib/perf/baseline-store
+ */
+
+const fs = require('fs');
+const path = require('path');
+const { getStateDir } = require('../platform/state-dir');
+const { validateBaseline, assertValid } = require('./schemas');
+
+const BASELINE_DIR = 'baselines';
+
+function assertSafeBaselineVersion(version) {
+ if (!version || typeof version !== 'string') {
+ throw new Error('Baseline version is required');
+ }
+ if (version.includes('..') || version.includes('/') || version.includes('\\') || version.includes('\0')) {
+ throw new Error('Baseline version contains invalid characters');
+ }
+ if (!/^[a-zA-Z0-9._+-]+$/.test(version)) {
+ throw new Error('Baseline version contains invalid characters');
+ }
+ return version;
+}
+
+/**
+ * Get baseline directory path
+ * @param {string} basePath
+ * @returns {string}
+ */
+function getBaselineDir(basePath = process.cwd()) {
+ return path.join(basePath, getStateDir(basePath), 'perf', BASELINE_DIR);
+}
+
+/**
+ * Ensure baseline directory exists
+ * @param {string} basePath
+ * @returns {string}
+ */
+function ensureBaselineDir(basePath = process.cwd()) {
+ const dir = getBaselineDir(basePath);
+ if (!fs.existsSync(dir)) {
+ fs.mkdirSync(dir, { recursive: true });
+ }
+ return dir;
+}
+
+/**
+ * Build baseline file path
+ * @param {string} version
+ * @param {string} basePath
+ * @returns {string}
+ */
+function getBaselinePath(version, basePath = process.cwd()) {
+ const safeVersion = assertSafeBaselineVersion(version);
+ return path.join(ensureBaselineDir(basePath), `${safeVersion}.json`);
+}
+
+/**
+ * List baseline versions
+ * @param {string} basePath
+ * @returns {string[]}
+ */
+function listBaselines(basePath = process.cwd()) {
+ const dir = ensureBaselineDir(basePath);
+ return fs.readdirSync(dir)
+ .filter(file => file.endsWith('.json'))
+ .map(file => path.basename(file, '.json'))
+ .sort();
+}
+
+/**
+ * Read baseline file
+ * @param {string} version
+ * @param {string} basePath
+ * @returns {object|null}
+ */
+function readBaseline(version, basePath = process.cwd()) {
+ const baselinePath = getBaselinePath(version, basePath);
+ if (!fs.existsSync(baselinePath)) {
+ return null;
+ }
+ try {
+ const parsed = JSON.parse(fs.readFileSync(baselinePath, 'utf8'));
+ const validation = validateBaseline(parsed);
+ if (!validation.ok) {
+ console.error(`[CRITICAL] Invalid baseline file at ${baselinePath}: ${validation.errors.join(', ')}`);
+ return null;
+ }
+ return parsed;
+ } catch (error) {
+ console.error(`[CRITICAL] Corrupted baseline file at ${baselinePath}: ${error.message}`);
+ return null;
+ }
+}
+
+/**
+ * Write baseline file (overwrites existing)
+ * @param {string} version
+ * @param {object} baseline
+ * @param {string} basePath
+ * @returns {boolean}
+ */
+function writeBaseline(version, baseline, basePath = process.cwd()) {
+ const baselinePath = getBaselinePath(version, basePath);
+ const payload = {
+ version,
+ recordedAt: new Date().toISOString(),
+ ...baseline
+ };
+ assertValid(validateBaseline(payload), 'Invalid baseline payload');
+ fs.writeFileSync(baselinePath, JSON.stringify(payload, null, 2), 'utf8');
+ return true;
+}
+
+module.exports = {
+ getBaselineDir,
+ ensureBaselineDir,
+ getBaselinePath,
+ listBaselines,
+ readBaseline,
+ writeBaseline
+};
diff --git a/lib/perf/benchmark-runner.js b/lib/perf/benchmark-runner.js
new file mode 100644
index 00000000..c245815c
--- /dev/null
+++ b/lib/perf/benchmark-runner.js
@@ -0,0 +1,107 @@
+/**
+ * Sequential benchmark runner utilities.
+ *
+ * @module lib/perf/benchmark-runner
+ */
+
+const { execSync } = require('child_process');
+const { validateBaseline } = require('./schemas');
+
+const DEFAULT_MIN_DURATION = 60;
+const BINARY_SEARCH_MIN_DURATION = 30;
+
+/**
+ * Normalize benchmark options and enforce minimum durations.
+ * @param {object} options
+ * @returns {object}
+ */
+function normalizeBenchmarkOptions(options = {}) {
+ const mode = options.mode || 'full';
+ const minDuration = mode === 'binary-search'
+ ? BINARY_SEARCH_MIN_DURATION
+ : DEFAULT_MIN_DURATION;
+
+ const duration = Math.max(options.duration || minDuration, minDuration);
+ return {
+ ...options,
+ mode,
+ duration,
+ warmup: options.warmup || 10
+ };
+}
+
+/**
+ * Run a benchmark command synchronously (sequential only).
+ * @param {string} command
+ * @param {object} options
+ * @returns {{ success: boolean, output: string }}
+ */
+function runBenchmark(command, options = {}) {
+ if (!command || typeof command !== 'string') {
+ throw new Error('Benchmark command must be a non-empty string');
+ }
+
+ const normalized = normalizeBenchmarkOptions(options);
+ const env = { ...process.env, ...normalized.env };
+
+ const output = execSync(command, {
+ stdio: 'pipe',
+ encoding: 'utf8',
+ env
+ });
+
+ return {
+ success: true,
+ output,
+ duration: normalized.duration,
+ warmup: normalized.warmup,
+ mode: normalized.mode
+ };
+}
+
+/**
+ * Parse metrics from benchmark output using PERF_METRICS markers.
+ * @param {string} output
+ * @returns {{ ok: boolean, metrics?: object, error?: string }}
+ */
+function parseMetrics(output) {
+ if (typeof output !== 'string') {
+ return { ok: false, error: 'Output must be a string' };
+ }
+
+ const startMarker = 'PERF_METRICS_START';
+ const endMarker = 'PERF_METRICS_END';
+ const startIndex = output.indexOf(startMarker);
+ const endIndex = output.indexOf(endMarker);
+
+ if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
+ return { ok: false, error: 'Metrics markers not found' };
+ }
+
+ const jsonStart = startIndex + startMarker.length;
+ const raw = output.slice(jsonStart, endIndex).trim();
+
+ try {
+ const parsed = JSON.parse(raw);
+ const validation = validateBaseline({
+ version: 'temp',
+ recordedAt: new Date().toISOString(),
+ command: 'temp',
+ metrics: parsed
+ });
+ if (!validation.ok) {
+ return { ok: false, error: `Invalid metrics: ${validation.errors.join(', ')}` };
+ }
+ return { ok: true, metrics: parsed };
+ } catch (error) {
+ return { ok: false, error: `Failed to parse metrics JSON: ${error.message}` };
+ }
+}
+
+module.exports = {
+ DEFAULT_MIN_DURATION,
+ BINARY_SEARCH_MIN_DURATION,
+ normalizeBenchmarkOptions,
+ runBenchmark,
+ parseMetrics
+};
diff --git a/lib/perf/breaking-point-finder.js b/lib/perf/breaking-point-finder.js
new file mode 100644
index 00000000..d7239cce
--- /dev/null
+++ b/lib/perf/breaking-point-finder.js
@@ -0,0 +1,52 @@
+/**
+ * Binary search helper for breaking point discovery.
+ *
+ * @module lib/perf/breaking-point-finder
+ */
+
+/**
+ * Find breaking point using binary search.
+ * The runner should return { ok: boolean, data?: any }.
+ *
+ * @param {object} options
+ * @param {number} options.min
+ * @param {number} options.max
+ * @param {(value:number)=>Promise<{ok:boolean,data?:any}>} options.runner
+ * @returns {Promise<{breakingPoint:number|null, attempts:number, history:Array}>}
+ */
+async function findBreakingPoint({ min, max, runner }) {
+ if (typeof min !== 'number' || typeof max !== 'number') {
+ throw new Error('min and max must be numbers');
+ }
+ if (typeof runner !== 'function') {
+ throw new Error('runner must be a function');
+ }
+
+ let low = min;
+ let high = max;
+ let breakingPoint = null;
+ const history = [];
+
+ while (low <= high) {
+ const mid = Math.floor((low + high) / 2);
+ const result = await runner(mid);
+ history.push({ value: mid, ok: result.ok });
+
+ if (result.ok) {
+ low = mid + 1;
+ } else {
+ breakingPoint = mid;
+ high = mid - 1;
+ }
+ }
+
+ return {
+ breakingPoint,
+ attempts: history.length,
+ history
+ };
+}
+
+module.exports = {
+ findBreakingPoint
+};
diff --git a/lib/perf/breaking-point-runner.js b/lib/perf/breaking-point-runner.js
new file mode 100644
index 00000000..0f15d5af
--- /dev/null
+++ b/lib/perf/breaking-point-runner.js
@@ -0,0 +1,60 @@
+/**
+ * Breaking point runner wrapper for /perf.
+ *
+ * @module lib/perf/breaking-point-runner
+ */
+
+const { runBenchmark, parseMetrics, BINARY_SEARCH_MIN_DURATION } = require('./benchmark-runner');
+const { findBreakingPoint } = require('./breaking-point-finder');
+
+/**
+ * Run a binary search to find the breaking point for a numeric parameter.
+ * The benchmark command should accept the value via an env var.
+ *
+ * @param {object} options
+ * @param {string} options.command
+ * @param {string} options.paramEnv
+ * @param {number} options.min
+ * @param {number} options.max
+ * @returns {Promise<{breakingPoint:number|null, attempts:number, history:Array}>}
+ */
+async function runBreakingPointSearch(options) {
+ const { command, paramEnv, min, max } = options || {};
+
+ if (!command || typeof command !== 'string') {
+ throw new Error('command must be a non-empty string');
+ }
+ if (!paramEnv || typeof paramEnv !== 'string') {
+ throw new Error('paramEnv must be a non-empty string');
+ }
+ if (typeof min !== 'number' || typeof max !== 'number') {
+ throw new Error('min and max must be numbers');
+ }
+
+ const runner = async (value) => {
+ try {
+ const result = runBenchmark(command, {
+ mode: 'binary-search',
+ duration: BINARY_SEARCH_MIN_DURATION,
+ env: {
+ [paramEnv]: String(value)
+ }
+ });
+
+ const parsed = parseMetrics(result.output);
+ if (!parsed.ok) {
+ return { ok: false, data: { error: parsed.error } };
+ }
+
+ return { ok: true, data: { metrics: parsed.metrics } };
+ } catch (error) {
+ return { ok: false, data: { error: error.message } };
+ }
+ };
+
+ return findBreakingPoint({ min, max, runner });
+}
+
+module.exports = {
+ runBreakingPointSearch
+};
diff --git a/lib/perf/checkpoint.js b/lib/perf/checkpoint.js
new file mode 100644
index 00000000..8926f855
--- /dev/null
+++ b/lib/perf/checkpoint.js
@@ -0,0 +1,99 @@
+/**
+ * Git checkpoint helper for /perf phases.
+ *
+ * @module lib/perf/checkpoint
+ */
+
+const { execSync, execFileSync } = require('child_process');
+
+/**
+ * Check if git repo is clean.
+ * @returns {boolean}
+ */
+function isWorkingTreeClean() {
+ const output = execSync('git status --porcelain', { encoding: 'utf8' }).trim();
+ return output.length === 0;
+}
+
+/**
+ * Build checkpoint commit message.
+ * @param {object} input
+ * @param {string} input.phase
+ * @param {string} input.id
+ * @param {string} [input.baselineVersion]
+ * @param {string} [input.deltaSummary]
+ * @returns {string}
+ */
+function buildCheckpointMessage(input) {
+ if (!input || typeof input !== 'object') {
+ throw new Error('Checkpoint input must be an object');
+ }
+ const { phase, id, baselineVersion, deltaSummary } = input;
+
+ if (!phase || typeof phase !== 'string') {
+ throw new Error('phase is required');
+ }
+ if (!id || typeof id !== 'string') {
+ throw new Error('id is required');
+ }
+
+ const baseline = baselineVersion || 'n/a';
+ const delta = deltaSummary || 'n/a';
+ return `perf: phase ${phase} [${id}] baseline=${baseline} delta=${delta}`;
+}
+
+/**
+ * Get the most recent git commit message.
+ * @returns {string|null}
+ */
+function getLastCommitMessage() {
+ try {
+ return execSync('git log -1 --pretty=%B', { encoding: 'utf8' }).trim();
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Check if the next checkpoint would duplicate the last commit.
+ * @param {string} message
+ * @returns {boolean}
+ */
+function isDuplicateCheckpoint(message) {
+ const last = getLastCommitMessage();
+ if (!last) return false;
+ return last.trim() === String(message || '').trim();
+}
+
+/**
+ * Commit a checkpoint for a perf phase.
+ * @param {object} input
+ * @returns {{ ok: boolean, message?: string, reason?: string }}
+ */
+function commitCheckpoint(input) {
+ try {
+ execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
+ } catch {
+ return { ok: false, reason: 'not a git repo' };
+ }
+
+ if (isWorkingTreeClean()) {
+ return { ok: false, reason: 'nothing to commit' };
+ }
+
+ const message = buildCheckpointMessage(input);
+ if (isDuplicateCheckpoint(message)) {
+ return { ok: false, reason: 'duplicate checkpoint' };
+ }
+ execFileSync('git', ['add', '-A'], { stdio: 'ignore' });
+ execFileSync('git', ['commit', '-m', message], { stdio: 'ignore' });
+ return { ok: true, message };
+}
+
+module.exports = {
+ isWorkingTreeClean,
+ buildCheckpointMessage,
+ getLastCommitMessage,
+ isDuplicateCheckpoint,
+ commitCheckpoint
+};
diff --git a/lib/perf/code-paths.js b/lib/perf/code-paths.js
new file mode 100644
index 00000000..ece2c8bf
--- /dev/null
+++ b/lib/perf/code-paths.js
@@ -0,0 +1,86 @@
+/**
+ * Code-path discovery helpers for /perf.
+ *
+ * @module lib/perf/code-paths
+ */
+
+const DEFAULT_STOPWORDS = new Set([
+ 'the', 'and', 'for', 'with', 'from', 'that', 'this', 'these', 'those',
+ 'into', 'over', 'under', 'than', 'then', 'when', 'where', 'what', 'which',
+ 'your', 'you', 'our', 'their', 'there', 'have', 'has', 'had', 'will',
+ 'would', 'should', 'could', 'about', 'across', 'after', 'before', 'while',
+ 'perf', 'performance', 'investigation', 'baseline', 'benchmark', 'scenario'
+]);
+
+function normalizeKeywords(text) {
+ if (!text || typeof text !== 'string') return [];
+ const tokens = text
+ .toLowerCase()
+ .split(/[^a-z0-9]+/g)
+ .filter(Boolean)
+ .filter(token => token.length > 2)
+ .filter(token => !DEFAULT_STOPWORDS.has(token));
+
+ return Array.from(new Set(tokens));
+}
+
+function scoreEntry(entry, keywords) {
+ let score = 0;
+ if (!entry || keywords.length === 0) return score;
+
+ const haystack = [
+ entry.file || '',
+ ...(entry.symbols || [])
+ ].join(' ').toLowerCase();
+
+ for (const keyword of keywords) {
+ if (haystack.includes(keyword)) score += 1;
+ }
+
+ return score;
+}
+
+function extractSymbols(fileData) {
+ if (!fileData || !fileData.symbols) return [];
+ const symbols = [];
+ for (const group of Object.values(fileData.symbols)) {
+ if (!Array.isArray(group)) continue;
+ for (const symbol of group) {
+ if (symbol && symbol.name) symbols.push(symbol.name);
+ }
+ }
+ return symbols;
+}
+
+function collectCodePaths(repoMap, scenario, limit = 12) {
+ if (!repoMap || !repoMap.files) {
+ return { keywords: normalizeKeywords(scenario), paths: [] };
+ }
+
+ const keywords = normalizeKeywords(scenario);
+ const candidates = [];
+
+ for (const [file, data] of Object.entries(repoMap.files)) {
+ const symbols = extractSymbols(data);
+ const entry = { file, symbols };
+ const score = scoreEntry(entry, keywords);
+ if (score <= 0) continue;
+ candidates.push({ ...entry, score });
+ }
+
+ candidates.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
+
+ return {
+ keywords,
+ paths: candidates.slice(0, limit).map(item => ({
+ file: item.file,
+ score: item.score,
+ symbols: item.symbols.slice(0, 8)
+ }))
+ };
+}
+
+module.exports = {
+ normalizeKeywords,
+ collectCodePaths
+};
diff --git a/lib/perf/consolidation.js b/lib/perf/consolidation.js
new file mode 100644
index 00000000..f8c292da
--- /dev/null
+++ b/lib/perf/consolidation.js
@@ -0,0 +1,37 @@
+/**
+ * Baseline consolidation helper.
+ *
+ * @module lib/perf/consolidation
+ */
+
+const baselineStore = require('./baseline-store');
+
+/**
+ * Consolidate a baseline for a version (overwrite existing).
+ * @param {object} input
+ * @param {string} input.version
+ * @param {object} input.baseline
+ * @param {string} [basePath]
+ * @returns {{ version: string, path: string }}
+ */
+function consolidateBaseline(input, basePath = process.cwd()) {
+ if (!input || typeof input !== 'object') {
+ throw new Error('consolidateBaseline requires an input object');
+ }
+ const { version, baseline } = input;
+
+ if (!version || typeof version !== 'string') {
+ throw new Error('version is required');
+ }
+ if (!baseline || typeof baseline !== 'object') {
+ throw new Error('baseline is required');
+ }
+
+ baselineStore.writeBaseline(version, baseline, basePath);
+ const path = baselineStore.getBaselinePath(version, basePath);
+ return { version, path };
+}
+
+module.exports = {
+ consolidateBaseline
+};
diff --git a/lib/perf/constraint-runner.js b/lib/perf/constraint-runner.js
new file mode 100644
index 00000000..a5c5f6a5
--- /dev/null
+++ b/lib/perf/constraint-runner.js
@@ -0,0 +1,69 @@
+/**
+ * Constraint testing runner for /perf.
+ *
+ * @module lib/perf/constraint-runner
+ */
+
+const { runBenchmark, parseMetrics, DEFAULT_MIN_DURATION } = require('./benchmark-runner');
+const { compareBaselines } = require('./baseline-comparator');
+
+/**
+ * Run baseline and constrained benchmarks sequentially.
+ * Constraints are provided via env vars to keep it cross-platform.
+ *
+ * @param {object} options
+ * @param {string} options.command
+ * @param {object} options.constraints
+ * @param {object} [options.env]
+ * @returns {{ constraints: object, baseline: object, constrained: object, delta: object }}
+ */
+function runConstraintTest(options) {
+ const { command, constraints, env } = options || {};
+
+ if (!command || typeof command !== 'string') {
+ throw new Error('command must be a non-empty string');
+ }
+ if (!constraints || typeof constraints !== 'object' || Array.isArray(constraints)) {
+ throw new Error('constraints must be an object');
+ }
+
+ const baselineResult = runBenchmark(command, {
+ duration: DEFAULT_MIN_DURATION,
+ env: {
+ ...env
+ }
+ });
+ const baselineMetrics = parseMetrics(baselineResult.output);
+ if (!baselineMetrics.ok) {
+ throw new Error(`Baseline metrics parse failed: ${baselineMetrics.error}`);
+ }
+
+ const constrainedResult = runBenchmark(command, {
+ duration: DEFAULT_MIN_DURATION,
+ env: {
+ ...env,
+ PERF_CPU_LIMIT: constraints.cpu,
+ PERF_MEMORY_LIMIT: constraints.memory
+ }
+ });
+ const constrainedMetrics = parseMetrics(constrainedResult.output);
+ if (!constrainedMetrics.ok) {
+ throw new Error(`Constrained metrics parse failed: ${constrainedMetrics.error}`);
+ }
+
+ const delta = compareBaselines(
+ { metrics: baselineMetrics.metrics },
+ { metrics: constrainedMetrics.metrics }
+ );
+
+ return {
+ constraints,
+ baseline: { metrics: baselineMetrics.metrics },
+ constrained: { metrics: constrainedMetrics.metrics },
+ delta
+ };
+}
+
+module.exports = {
+ runConstraintTest
+};
diff --git a/lib/perf/experiment-runner.js b/lib/perf/experiment-runner.js
new file mode 100644
index 00000000..fbee670d
--- /dev/null
+++ b/lib/perf/experiment-runner.js
@@ -0,0 +1,32 @@
+/**
+ * Experiment runner utilities.
+ *
+ * @module lib/perf/experiment-runner
+ */
+
+/**
+ * Run experiments sequentially (never parallel).
+ * @param {Array