agentsys 5.0.3 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +21 -14
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +18 -0
- package/README.md +7 -6
- package/adapters/codex/skills/agnix/SKILL.md +0 -1
- package/adapters/codex/skills/audit-project/SKILL.md +0 -1
- package/adapters/codex/skills/audit-project-agents/SKILL.md +0 -1
- package/adapters/codex/skills/audit-project-github/SKILL.md +0 -1
- package/adapters/codex/skills/consult/SKILL.md +132 -57
- package/adapters/codex/skills/debate/SKILL.md +214 -0
- package/adapters/codex/skills/delivery-approval/SKILL.md +0 -1
- package/adapters/codex/skills/deslop/SKILL.md +0 -1
- package/adapters/codex/skills/drift-detect/SKILL.md +0 -1
- package/adapters/codex/skills/enhance/SKILL.md +0 -1
- package/adapters/codex/skills/learn/SKILL.md +0 -1
- package/adapters/codex/skills/next-task/SKILL.md +0 -1
- package/adapters/codex/skills/perf/SKILL.md +0 -1
- package/adapters/codex/skills/repo-map/SKILL.md +0 -1
- package/adapters/codex/skills/ship/SKILL.md +0 -1
- package/adapters/codex/skills/ship-ci-review-loop/SKILL.md +0 -1
- package/adapters/codex/skills/ship-deployment/SKILL.md +0 -1
- package/adapters/codex/skills/ship-error-handling/SKILL.md +0 -1
- package/adapters/codex/skills/sync-docs/SKILL.md +0 -1
- package/adapters/opencode/agents/agent-enhancer.md +0 -1
- package/adapters/opencode/agents/agnix-agent.md +0 -1
- package/adapters/opencode/agents/ci-fixer.md +0 -1
- package/adapters/opencode/agents/ci-monitor.md +0 -1
- package/adapters/opencode/agents/claudemd-enhancer.md +0 -1
- package/adapters/opencode/agents/consult-agent.md +122 -30
- package/adapters/opencode/agents/cross-file-enhancer.md +0 -1
- package/adapters/opencode/agents/debate-orchestrator.md +169 -0
- package/adapters/opencode/agents/delivery-validator.md +0 -1
- package/adapters/opencode/agents/deslop-agent.md +0 -1
- package/adapters/opencode/agents/docs-enhancer.md +0 -1
- package/adapters/opencode/agents/exploration-agent.md +0 -1
- package/adapters/opencode/agents/hooks-enhancer.md +0 -1
- package/adapters/opencode/agents/implementation-agent.md +0 -1
- package/adapters/opencode/agents/learn-agent.md +0 -1
- package/adapters/opencode/agents/map-validator.md +0 -1
- package/adapters/opencode/agents/perf-analyzer.md +0 -1
- package/adapters/opencode/agents/perf-code-paths.md +0 -1
- package/adapters/opencode/agents/perf-investigation-logger.md +0 -1
- package/adapters/opencode/agents/perf-orchestrator.md +0 -1
- package/adapters/opencode/agents/perf-theory-gatherer.md +0 -1
- package/adapters/opencode/agents/perf-theory-tester.md +0 -1
- package/adapters/opencode/agents/plan-synthesizer.md +0 -1
- package/adapters/opencode/agents/planning-agent.md +0 -1
- package/adapters/opencode/agents/plugin-enhancer.md +0 -1
- package/adapters/opencode/agents/prompt-enhancer.md +0 -1
- package/adapters/opencode/agents/simple-fixer.md +0 -1
- package/adapters/opencode/agents/skills-enhancer.md +0 -1
- package/adapters/opencode/agents/sync-docs-agent.md +0 -1
- package/adapters/opencode/agents/task-discoverer.md +0 -1
- package/adapters/opencode/agents/test-coverage-checker.md +0 -1
- package/adapters/opencode/agents/worktree-manager.md +0 -1
- package/adapters/opencode/commands/agnix.md +0 -1
- package/adapters/opencode/commands/audit-project-agents.md +0 -1
- package/adapters/opencode/commands/audit-project-github.md +0 -1
- package/adapters/opencode/commands/audit-project.md +0 -1
- package/adapters/opencode/commands/consult.md +133 -57
- package/adapters/opencode/commands/debate.md +224 -0
- package/adapters/opencode/commands/delivery-approval.md +0 -1
- package/adapters/opencode/commands/deslop.md +0 -1
- package/adapters/opencode/commands/drift-detect.md +0 -1
- package/adapters/opencode/commands/enhance.md +0 -1
- package/adapters/opencode/commands/learn.md +0 -1
- package/adapters/opencode/commands/next-task.md +0 -1
- package/adapters/opencode/commands/perf.md +0 -1
- package/adapters/opencode/commands/repo-map.md +0 -1
- package/adapters/opencode/commands/ship-ci-review-loop.md +0 -1
- package/adapters/opencode/commands/ship-deployment.md +0 -1
- package/adapters/opencode/commands/ship-error-handling.md +0 -1
- package/adapters/opencode/commands/ship.md +0 -1
- package/adapters/opencode/commands/sync-docs.md +0 -1
- package/adapters/opencode/skills/agnix/SKILL.md +1 -2
- package/adapters/opencode/skills/consult/SKILL.md +33 -23
- package/adapters/opencode/skills/debate/SKILL.md +245 -0
- package/adapters/opencode/skills/deslop/SKILL.md +1 -2
- package/adapters/opencode/skills/discover-tasks/SKILL.md +1 -2
- package/adapters/opencode/skills/drift-analysis/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-agent-prompts/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-claude-memory/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-cross-file/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-docs/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-hooks/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-orchestrator/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-plugins/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-prompts/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-skills/SKILL.md +1 -2
- package/adapters/opencode/skills/learn/SKILL.md +1 -2
- package/adapters/opencode/skills/orchestrate-review/SKILL.md +0 -1
- package/adapters/opencode/skills/perf-analyzer/SKILL.md +1 -2
- package/adapters/opencode/skills/perf-baseline-manager/SKILL.md +1 -2
- package/adapters/opencode/skills/perf-benchmarker/SKILL.md +1 -2
- package/adapters/opencode/skills/perf-code-paths/SKILL.md +1 -2
- package/adapters/opencode/skills/perf-investigation-logger/SKILL.md +1 -2
- package/adapters/opencode/skills/perf-profiler/SKILL.md +1 -2
- package/adapters/opencode/skills/perf-theory-gatherer/SKILL.md +1 -2
- package/adapters/opencode/skills/perf-theory-tester/SKILL.md +1 -2
- package/adapters/opencode/skills/repo-mapping/SKILL.md +1 -2
- package/adapters/opencode/skills/sync-docs/SKILL.md +1 -2
- package/adapters/opencode/skills/validate-delivery/SKILL.md +1 -2
- package/lib/adapter-transforms.js +24 -4
- package/package.json +1 -1
- package/plugins/agnix/.claude-plugin/plugin.json +1 -1
- package/plugins/agnix/skills/agnix/SKILL.md +1 -1
- package/plugins/audit-project/.claude-plugin/plugin.json +1 -1
- package/plugins/audit-project/lib/adapter-transforms.js +24 -4
- package/plugins/consult/.claude-plugin/plugin.json +1 -1
- package/plugins/consult/agents/consult-agent.md +122 -29
- package/plugins/consult/commands/consult.md +135 -58
- package/plugins/consult/skills/consult/SKILL.md +31 -20
- package/plugins/debate/.claude-plugin/plugin.json +21 -0
- package/plugins/debate/agents/debate-orchestrator.md +175 -0
- package/plugins/debate/commands/debate.md +221 -0
- package/plugins/debate/lib/adapter-transforms.js +298 -0
- package/plugins/debate/lib/collectors/codebase.js +392 -0
- package/plugins/debate/lib/collectors/docs-patterns.js +713 -0
- package/plugins/debate/lib/collectors/documentation.js +219 -0
- package/plugins/debate/lib/collectors/github.js +330 -0
- package/plugins/debate/lib/collectors/index.js +126 -0
- package/plugins/debate/lib/config/index.js +14 -0
- package/plugins/debate/lib/cross-platform/index.js +539 -0
- package/plugins/debate/lib/discovery/index.js +352 -0
- package/plugins/debate/lib/drift-detect/collectors.js +37 -0
- package/plugins/debate/lib/enhance/agent-analyzer.js +421 -0
- package/plugins/debate/lib/enhance/agent-patterns.js +571 -0
- package/plugins/debate/lib/enhance/auto-suppression.js +622 -0
- package/plugins/debate/lib/enhance/benchmark.js +417 -0
- package/plugins/debate/lib/enhance/cross-file-analyzer.js +930 -0
- package/plugins/debate/lib/enhance/cross-file-patterns.js +370 -0
- package/plugins/debate/lib/enhance/docs-analyzer.js +325 -0
- package/plugins/debate/lib/enhance/docs-patterns.js +671 -0
- package/plugins/debate/lib/enhance/fixer.js +721 -0
- package/plugins/debate/lib/enhance/hook-analyzer.js +135 -0
- package/plugins/debate/lib/enhance/hook-patterns.js +40 -0
- package/plugins/debate/lib/enhance/index.js +127 -0
- package/plugins/debate/lib/enhance/plugin-analyzer.js +402 -0
- package/plugins/debate/lib/enhance/plugin-patterns.js +326 -0
- package/plugins/debate/lib/enhance/projectmemory-analyzer.js +551 -0
- package/plugins/debate/lib/enhance/projectmemory-patterns.js +617 -0
- package/plugins/debate/lib/enhance/prompt-analyzer.js +457 -0
- package/plugins/debate/lib/enhance/prompt-patterns.js +1484 -0
- package/plugins/debate/lib/enhance/reporter.js +1348 -0
- package/plugins/debate/lib/enhance/security-patterns.js +284 -0
- package/plugins/debate/lib/enhance/skill-analyzer.js +182 -0
- package/plugins/debate/lib/enhance/skill-patterns.js +147 -0
- package/plugins/debate/lib/enhance/suppression.js +352 -0
- package/plugins/debate/lib/enhance/tool-patterns.js +373 -0
- package/plugins/debate/lib/index.js +270 -0
- package/plugins/debate/lib/patterns/cli-enhancers.js +611 -0
- package/plugins/debate/lib/patterns/pipeline.js +948 -0
- package/plugins/debate/lib/patterns/review-patterns.js +558 -0
- package/plugins/debate/lib/patterns/slop-analyzers.js +2305 -0
- package/plugins/debate/lib/patterns/slop-patterns.js +1187 -0
- package/plugins/debate/lib/perf/analyzer/index.js +22 -0
- package/plugins/debate/lib/perf/argument-parser.js +105 -0
- package/plugins/debate/lib/perf/baseline-comparator.js +50 -0
- package/plugins/debate/lib/perf/baseline-store.js +127 -0
- package/plugins/debate/lib/perf/benchmark-runner.js +404 -0
- package/plugins/debate/lib/perf/breaking-point-finder.js +52 -0
- package/plugins/debate/lib/perf/breaking-point-runner.js +60 -0
- package/plugins/debate/lib/perf/checkpoint.js +123 -0
- package/plugins/debate/lib/perf/code-paths.js +86 -0
- package/plugins/debate/lib/perf/consolidation.js +37 -0
- package/plugins/debate/lib/perf/constraint-runner.js +71 -0
- package/plugins/debate/lib/perf/experiment-runner.js +32 -0
- package/plugins/debate/lib/perf/index.js +41 -0
- package/plugins/debate/lib/perf/investigation-state.js +874 -0
- package/plugins/debate/lib/perf/optimization-runner.js +79 -0
- package/plugins/debate/lib/perf/profilers/go.js +22 -0
- package/plugins/debate/lib/perf/profilers/index.js +46 -0
- package/plugins/debate/lib/perf/profilers/java.js +23 -0
- package/plugins/debate/lib/perf/profilers/node.js +27 -0
- package/plugins/debate/lib/perf/profilers/python.js +23 -0
- package/plugins/debate/lib/perf/profilers/rust.js +23 -0
- package/plugins/debate/lib/perf/profiling-runner.js +75 -0
- package/plugins/debate/lib/perf/schemas.js +140 -0
- package/plugins/debate/lib/platform/detect-platform.js +413 -0
- package/plugins/debate/lib/platform/detection-configs.js +93 -0
- package/plugins/debate/lib/platform/state-dir.js +132 -0
- package/plugins/debate/lib/platform/verify-tools.js +182 -0
- package/plugins/debate/lib/repo-map/cache.js +152 -0
- package/plugins/debate/lib/repo-map/concurrency.js +29 -0
- package/plugins/debate/lib/repo-map/index.js +222 -0
- package/plugins/debate/lib/repo-map/installer.js +212 -0
- package/plugins/debate/lib/repo-map/queries/go.js +27 -0
- package/plugins/debate/lib/repo-map/queries/index.js +100 -0
- package/plugins/debate/lib/repo-map/queries/java.js +38 -0
- package/plugins/debate/lib/repo-map/queries/javascript.js +55 -0
- package/plugins/debate/lib/repo-map/queries/python.js +24 -0
- package/plugins/debate/lib/repo-map/queries/rust.js +73 -0
- package/plugins/debate/lib/repo-map/queries/typescript.js +38 -0
- package/plugins/debate/lib/repo-map/runner.js +1364 -0
- package/plugins/debate/lib/repo-map/updater.js +562 -0
- package/plugins/debate/lib/repo-map/usage-analyzer.js +407 -0
- package/plugins/debate/lib/schemas/plugin-manifest.schema.json +57 -0
- package/plugins/debate/lib/schemas/validator.js +247 -0
- package/plugins/debate/lib/sources/custom-handler.js +199 -0
- package/plugins/debate/lib/sources/policy-questions.js +246 -0
- package/plugins/debate/lib/sources/source-cache.js +165 -0
- package/plugins/debate/lib/state/workflow-state.js +576 -0
- package/plugins/debate/lib/types/agent-frontmatter.d.ts +134 -0
- package/plugins/debate/lib/types/command-frontmatter.d.ts +107 -0
- package/plugins/debate/lib/types/hook-frontmatter.d.ts +115 -0
- package/plugins/debate/lib/types/index.d.ts +84 -0
- package/plugins/debate/lib/types/plugin-manifest.d.ts +102 -0
- package/plugins/debate/lib/types/skill-frontmatter.d.ts +89 -0
- package/plugins/debate/lib/utils/atomic-write.js +94 -0
- package/plugins/debate/lib/utils/cache-manager.js +159 -0
- package/plugins/debate/lib/utils/command-parser.js +0 -0
- package/plugins/debate/lib/utils/context-optimizer.js +300 -0
- package/plugins/debate/lib/utils/deprecation.js +37 -0
- package/plugins/debate/lib/utils/shell-escape.js +88 -0
- package/plugins/debate/lib/utils/state-helpers.js +61 -0
- package/plugins/debate/skills/debate/SKILL.md +264 -0
- package/plugins/deslop/.claude-plugin/plugin.json +1 -1
- package/plugins/deslop/lib/adapter-transforms.js +24 -4
- package/plugins/deslop/skills/deslop/SKILL.md +1 -1
- package/plugins/drift-detect/.claude-plugin/plugin.json +1 -1
- package/plugins/drift-detect/lib/adapter-transforms.js +24 -4
- package/plugins/drift-detect/skills/drift-analysis/SKILL.md +1 -1
- package/plugins/enhance/.claude-plugin/plugin.json +1 -1
- package/plugins/enhance/lib/adapter-transforms.js +24 -4
- package/plugins/enhance/skills/enhance-agent-prompts/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-claude-memory/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-cross-file/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-docs/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-hooks/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-orchestrator/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-plugins/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-prompts/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-skills/SKILL.md +1 -1
- package/plugins/learn/.claude-plugin/plugin.json +1 -1
- package/plugins/learn/agents/learn-agent.md +1 -1
- package/plugins/learn/lib/adapter-transforms.js +24 -4
- package/plugins/learn/skills/learn/SKILL.md +1 -1
- package/plugins/next-task/.claude-plugin/plugin.json +1 -1
- package/plugins/next-task/agents/exploration-agent.md +1 -1
- package/plugins/next-task/lib/adapter-transforms.js +24 -4
- package/plugins/next-task/skills/discover-tasks/SKILL.md +1 -1
- package/plugins/next-task/skills/validate-delivery/SKILL.md +1 -1
- package/plugins/perf/.claude-plugin/plugin.json +1 -1
- package/plugins/perf/lib/adapter-transforms.js +24 -4
- package/plugins/perf/skills/perf-analyzer/SKILL.md +1 -1
- package/plugins/perf/skills/perf-baseline-manager/SKILL.md +1 -1
- package/plugins/perf/skills/perf-benchmarker/SKILL.md +1 -1
- package/plugins/perf/skills/perf-code-paths/SKILL.md +1 -1
- package/plugins/perf/skills/perf-investigation-logger/SKILL.md +1 -1
- package/plugins/perf/skills/perf-profiler/SKILL.md +1 -1
- package/plugins/perf/skills/perf-theory-gatherer/SKILL.md +1 -1
- package/plugins/perf/skills/perf-theory-tester/SKILL.md +1 -1
- package/plugins/repo-map/.claude-plugin/plugin.json +1 -1
- package/plugins/repo-map/lib/adapter-transforms.js +24 -4
- package/plugins/ship/.claude-plugin/plugin.json +1 -1
- package/plugins/ship/lib/adapter-transforms.js +24 -4
- package/plugins/sync-docs/.claude-plugin/plugin.json +1 -1
- package/plugins/sync-docs/lib/adapter-transforms.js +24 -4
- package/plugins/sync-docs/skills/sync-docs/SKILL.md +1 -1
- package/scripts/gen-adapters.js +6 -7
- package/scripts/generate-docs.js +4 -2
- package/scripts/plugins.txt +1 -0
- package/site/content.json +6 -6
|
@@ -0,0 +1,930 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-File Semantic Analyzer
|
|
3
|
+
* Analyzes relationships between agents, skills, and workflows
|
|
4
|
+
*
|
|
5
|
+
* Cross-platform compatible: Works with Claude Code, OpenCode, and Codex
|
|
6
|
+
*
|
|
7
|
+
* @author Avi Fenesh
|
|
8
|
+
* @license MIT
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { parseMarkdownFrontmatter } = require('./agent-analyzer');
|
|
14
|
+
const { crossFilePatterns, loadKnownTools } = require('./cross-file-patterns');
|
|
15
|
+
|
|
16
|
+
// ============================================
|
|
17
|
+
// CONSTANTS
|
|
18
|
+
// ============================================
|
|
19
|
+
|
|
20
|
+
/** Minimum instruction length to consider for duplicate/contradiction detection */
|
|
21
|
+
const MIN_INSTRUCTION_LENGTH = 20;
|
|
22
|
+
|
|
23
|
+
/** Minimum number of files for duplicate instruction flagging */
|
|
24
|
+
const MIN_DUPLICATE_COUNT = 3;
|
|
25
|
+
|
|
26
|
+
/** Similarity threshold for contradiction detection (0-1) */
|
|
27
|
+
const CONTRADICTION_SIMILARITY_THRESHOLD = 0.6;
|
|
28
|
+
|
|
29
|
+
/** Length of action text to compare for contradictions */
|
|
30
|
+
const ACTION_COMPARISON_LENGTH = 30;
|
|
31
|
+
|
|
32
|
+
/** Minimum word length for similarity calculation (filters noise) */
|
|
33
|
+
const MIN_WORD_LENGTH = 3;
|
|
34
|
+
|
|
35
|
+
/** Default analysis categories */
|
|
36
|
+
const DEFAULT_ANALYSIS_CATEGORIES = ['tool-consistency', 'workflow', 'consistency', 'skill-alignment'];
|
|
37
|
+
|
|
38
|
+
// ============================================
|
|
39
|
+
// UTILITY FUNCTIONS
|
|
40
|
+
// ============================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Escape regex special characters in a string
|
|
44
|
+
* @param {string} str - String to escape
|
|
45
|
+
* @returns {string} Escaped string safe for RegExp
|
|
46
|
+
*/
|
|
47
|
+
function escapeRegex(str) {
|
|
48
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Validate that a path is within the expected root directory
|
|
53
|
+
* Prevents path traversal attacks
|
|
54
|
+
* @param {string} targetPath - Path to validate
|
|
55
|
+
* @param {string} rootDir - Expected root directory
|
|
56
|
+
* @returns {boolean} True if path is safe
|
|
57
|
+
*/
|
|
58
|
+
function isPathWithinRoot(targetPath, rootDir) {
|
|
59
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
60
|
+
const resolvedRoot = path.resolve(rootDir);
|
|
61
|
+
return resolvedTarget.startsWith(resolvedRoot + path.sep) || resolvedTarget === resolvedRoot;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Calculate simple string similarity (Jaccard index on words)
|
|
66
|
+
* @param {string} a - First string
|
|
67
|
+
* @param {string} b - Second string
|
|
68
|
+
* @returns {number} Similarity score 0-1
|
|
69
|
+
*/
|
|
70
|
+
function calculateSimilarity(a, b) {
|
|
71
|
+
if (!a || !b) return 0;
|
|
72
|
+
|
|
73
|
+
const wordsA = new Set(a.split(/\s+/).filter(w => w.length >= MIN_WORD_LENGTH));
|
|
74
|
+
const wordsB = new Set(b.split(/\s+/).filter(w => w.length >= MIN_WORD_LENGTH));
|
|
75
|
+
|
|
76
|
+
if (wordsA.size === 0 || wordsB.size === 0) return 0;
|
|
77
|
+
|
|
78
|
+
let intersection = 0;
|
|
79
|
+
for (const word of wordsA) {
|
|
80
|
+
if (wordsB.has(word)) intersection++;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const union = wordsA.size + wordsB.size - intersection;
|
|
84
|
+
return intersection / union;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ============================================
|
|
88
|
+
// PRE-COMPILED PATTERNS
|
|
89
|
+
// ============================================
|
|
90
|
+
|
|
91
|
+
/** Pre-compiled shell command patterns for Bash detection */
|
|
92
|
+
const SHELL_PATTERNS = [
|
|
93
|
+
/\bgit\s+(?:add|commit|push|pull|branch|checkout|merge|rebase|status|diff|log)\b/i,
|
|
94
|
+
/\bnpm\s+(?:install|test|run|build|publish)\b/i,
|
|
95
|
+
/\bpnpm\s+/i,
|
|
96
|
+
/\byarn\s+/i,
|
|
97
|
+
/\bcargo\s+/i,
|
|
98
|
+
/\bgo\s+(?:build|test|run|mod)\b/i
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
/** Pre-compiled critical instruction patterns */
|
|
102
|
+
const CRITICAL_PATTERNS = [
|
|
103
|
+
/\bMUST\b/,
|
|
104
|
+
/\bNEVER\b/,
|
|
105
|
+
/\bALWAYS\b/i,
|
|
106
|
+
/\bREQUIRED\b/i,
|
|
107
|
+
/\bFORBIDDEN\b/i,
|
|
108
|
+
/\bCRITICAL\b/i,
|
|
109
|
+
/\bDO NOT\b/i,
|
|
110
|
+
/\bdon't\b/i
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
/** Pre-compiled pattern for extracting agent references */
|
|
114
|
+
const SUBAGENT_PATTERN = /subagent_type\s*[=:]\s*["']([^"']+)["']/g;
|
|
115
|
+
|
|
116
|
+
/** Pre-compiled patterns for cleaning content */
|
|
117
|
+
const BAD_EXAMPLE_TAG_PATTERN = /<bad[_\- ]?example>[\s\S]*?<\/bad[_\- ]?example>/gi;
|
|
118
|
+
const BAD_EXAMPLE_CODE_PATTERN = /```[^\n]*bad[^\n]*\n[\s\S]*?```/gi;
|
|
119
|
+
|
|
120
|
+
// ============================================
|
|
121
|
+
// TOOL PATTERN CACHE
|
|
122
|
+
// ============================================
|
|
123
|
+
|
|
124
|
+
/** Cache for compiled tool patterns */
|
|
125
|
+
const toolPatternCache = new Map();
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get or create compiled patterns for a tool name
|
|
129
|
+
* @param {string} tool - Tool name
|
|
130
|
+
* @returns {Object} Compiled patterns for the tool
|
|
131
|
+
*/
|
|
132
|
+
function getToolPatterns(tool) {
|
|
133
|
+
if (toolPatternCache.has(tool)) {
|
|
134
|
+
return toolPatternCache.get(tool);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const escapedTool = escapeRegex(tool);
|
|
138
|
+
const patterns = {
|
|
139
|
+
call: new RegExp(`\\b${escapedTool}\\s*\\(`),
|
|
140
|
+
mention: new RegExp(`\\b(?:use|invoke|call|with)\\s+(?:the\\s+)?${escapedTool}\\b`, 'i'),
|
|
141
|
+
noun: new RegExp(`\\b${escapedTool}\\s+tool\\b`, 'i')
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
toolPatternCache.set(tool, patterns);
|
|
145
|
+
return patterns;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ============================================
|
|
149
|
+
// EXTRACTION FUNCTIONS
|
|
150
|
+
// ============================================
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Extract tool mentions from prompt content
|
|
154
|
+
* Detects tool usage patterns in prompt body
|
|
155
|
+
* @param {string} content - Prompt content
|
|
156
|
+
* @param {string[]} knownTools - List of known tool names
|
|
157
|
+
* @returns {string[]} Array of tool names found
|
|
158
|
+
*/
|
|
159
|
+
function extractToolMentions(content, knownTools) {
|
|
160
|
+
if (!content || typeof content !== 'string') return [];
|
|
161
|
+
if (!Array.isArray(knownTools)) return [];
|
|
162
|
+
|
|
163
|
+
const found = new Set();
|
|
164
|
+
|
|
165
|
+
// Skip content inside bad-example tags and code blocks with "bad" in info string
|
|
166
|
+
const cleanContent = content
|
|
167
|
+
.replace(BAD_EXAMPLE_TAG_PATTERN, '')
|
|
168
|
+
.replace(BAD_EXAMPLE_CODE_PATTERN, '');
|
|
169
|
+
|
|
170
|
+
for (const tool of knownTools) {
|
|
171
|
+
const patterns = getToolPatterns(tool);
|
|
172
|
+
|
|
173
|
+
// Pattern 1: Tool({ or Tool(
|
|
174
|
+
if (patterns.call.test(cleanContent)) {
|
|
175
|
+
found.add(tool);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Pattern 2: "use the Tool tool" or "invoke Tool"
|
|
180
|
+
if (patterns.mention.test(cleanContent)) {
|
|
181
|
+
found.add(tool);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Pattern 3: Tool tool (e.g., "Read tool", "Bash tool")
|
|
186
|
+
if (patterns.noun.test(cleanContent)) {
|
|
187
|
+
found.add(tool);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Special case: Bash detection via shell commands
|
|
192
|
+
if (!found.has('Bash') && !found.has('Shell')) {
|
|
193
|
+
for (const pattern of SHELL_PATTERNS) {
|
|
194
|
+
if (pattern.test(cleanContent)) {
|
|
195
|
+
found.add('Bash');
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return Array.from(found);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Extract agent references from content
|
|
206
|
+
* Finds subagent_type references in Task() calls
|
|
207
|
+
* @param {string} content - Content to scan
|
|
208
|
+
* @returns {string[]} Array of referenced agent names (plugin:agent format)
|
|
209
|
+
*/
|
|
210
|
+
function extractAgentReferences(content) {
|
|
211
|
+
if (!content || typeof content !== 'string') return [];
|
|
212
|
+
|
|
213
|
+
const references = new Set();
|
|
214
|
+
|
|
215
|
+
// Use matchAll for safer iteration (no lastIndex issues)
|
|
216
|
+
const matches = content.matchAll(new RegExp(SUBAGENT_PATTERN.source, 'g'));
|
|
217
|
+
for (const match of matches) {
|
|
218
|
+
references.add(match[1]);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return Array.from(references);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Extract critical instructions (lines with MUST, NEVER, always, etc.)
|
|
226
|
+
* @param {string} content - Content to scan
|
|
227
|
+
* @returns {Array<{line: string, lineNumber: number}>} Critical instructions
|
|
228
|
+
*/
|
|
229
|
+
function extractCriticalInstructions(content) {
|
|
230
|
+
if (!content || typeof content !== 'string') return [];
|
|
231
|
+
|
|
232
|
+
const instructions = [];
|
|
233
|
+
const lines = content.split('\n');
|
|
234
|
+
let inCodeBlock = false;
|
|
235
|
+
|
|
236
|
+
for (let i = 0; i < lines.length; i++) {
|
|
237
|
+
const line = lines[i].trim();
|
|
238
|
+
|
|
239
|
+
// Track code block state
|
|
240
|
+
if (line.startsWith('```')) {
|
|
241
|
+
inCodeBlock = !inCodeBlock;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Skip empty lines, headers, and content inside code blocks
|
|
246
|
+
if (!line || line.startsWith('#') || inCodeBlock) continue;
|
|
247
|
+
|
|
248
|
+
for (const pattern of CRITICAL_PATTERNS) {
|
|
249
|
+
if (pattern.test(line)) {
|
|
250
|
+
instructions.push({ line, lineNumber: i + 1 });
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return instructions;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ============================================
|
|
260
|
+
// FILE LOADING FUNCTIONS
|
|
261
|
+
// ============================================
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Generic function to load prompt files from plugins directory
|
|
265
|
+
* @param {string} rootDir - Root directory
|
|
266
|
+
* @param {string} subDir - Subdirectory name ('agents' or 'skills')
|
|
267
|
+
* @param {Function} fileFilter - Function to filter files
|
|
268
|
+
* @param {Function} pathBuilder - Function to build file path
|
|
269
|
+
* @param {Object} options - Options including verbose flag
|
|
270
|
+
* @returns {Array<Object>} Array of parsed prompt objects
|
|
271
|
+
*/
|
|
272
|
+
function loadPromptFiles(rootDir, subDir, fileFilter, pathBuilder, options = {}) {
|
|
273
|
+
const results = [];
|
|
274
|
+
const errors = [];
|
|
275
|
+
|
|
276
|
+
const pluginsDir = path.join(rootDir, 'plugins');
|
|
277
|
+
|
|
278
|
+
// Validate path is within root
|
|
279
|
+
if (!isPathWithinRoot(pluginsDir, rootDir)) {
|
|
280
|
+
if (options.verbose) {
|
|
281
|
+
console.error('[cross-file] Invalid plugins directory path');
|
|
282
|
+
}
|
|
283
|
+
return results;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Check if plugins directory exists
|
|
287
|
+
try {
|
|
288
|
+
fs.accessSync(pluginsDir);
|
|
289
|
+
} catch {
|
|
290
|
+
return results;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let plugins;
|
|
294
|
+
try {
|
|
295
|
+
plugins = fs.readdirSync(pluginsDir).filter(f => {
|
|
296
|
+
const fullPath = path.join(pluginsDir, f);
|
|
297
|
+
if (!isPathWithinRoot(fullPath, rootDir)) return false;
|
|
298
|
+
try {
|
|
299
|
+
return fs.statSync(fullPath).isDirectory();
|
|
300
|
+
} catch {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
} catch (err) {
|
|
305
|
+
if (options.verbose) {
|
|
306
|
+
console.error('[cross-file] Failed to read plugins directory:', err.message);
|
|
307
|
+
}
|
|
308
|
+
return results;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
for (const plugin of plugins) {
|
|
312
|
+
const targetDir = path.join(pluginsDir, plugin, subDir);
|
|
313
|
+
|
|
314
|
+
// Validate path
|
|
315
|
+
if (!isPathWithinRoot(targetDir, rootDir)) continue;
|
|
316
|
+
|
|
317
|
+
// Check if directory exists
|
|
318
|
+
try {
|
|
319
|
+
fs.accessSync(targetDir);
|
|
320
|
+
} catch {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
let files;
|
|
325
|
+
try {
|
|
326
|
+
files = fs.readdirSync(targetDir).filter(fileFilter);
|
|
327
|
+
} catch (err) {
|
|
328
|
+
if (options.verbose) {
|
|
329
|
+
errors.push({ path: targetDir, error: err.message });
|
|
330
|
+
}
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
for (const file of files) {
|
|
335
|
+
const filePath = pathBuilder(targetDir, file);
|
|
336
|
+
|
|
337
|
+
// Validate path
|
|
338
|
+
if (!isPathWithinRoot(filePath, rootDir)) continue;
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
342
|
+
const { frontmatter, body } = parseMarkdownFrontmatter(content);
|
|
343
|
+
|
|
344
|
+
results.push({
|
|
345
|
+
plugin,
|
|
346
|
+
name: file.replace('.md', '').replace(/^SKILL$/, path.basename(path.dirname(filePath))),
|
|
347
|
+
path: filePath,
|
|
348
|
+
frontmatter: frontmatter || {},
|
|
349
|
+
body,
|
|
350
|
+
content
|
|
351
|
+
});
|
|
352
|
+
} catch (err) {
|
|
353
|
+
if (options.verbose) {
|
|
354
|
+
errors.push({ path: filePath, error: err.message });
|
|
355
|
+
}
|
|
356
|
+
// Skip files that can't be read - intentional for robustness
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (options.verbose && errors.length > 0) {
|
|
362
|
+
console.error('[cross-file] File read errors:', errors);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return results;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Load all agent files from a directory structure
|
|
370
|
+
* @param {string} rootDir - Root directory to scan
|
|
371
|
+
* @param {Object} options - Options including verbose flag
|
|
372
|
+
* @returns {Array<Object>} Array of parsed agent objects
|
|
373
|
+
*/
|
|
374
|
+
function loadAllAgents(rootDir, options = {}) {
|
|
375
|
+
return loadPromptFiles(
|
|
376
|
+
rootDir,
|
|
377
|
+
'agents',
|
|
378
|
+
f => f.endsWith('.md') && f.toLowerCase() !== 'readme.md',
|
|
379
|
+
(dir, file) => path.join(dir, file),
|
|
380
|
+
options
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Load all command files from a directory structure
|
|
386
|
+
* @param {string} rootDir - Root directory to scan
|
|
387
|
+
* @param {Object} options - Options including verbose flag
|
|
388
|
+
* @returns {Array<Object>} Array of parsed command objects
|
|
389
|
+
*/
|
|
390
|
+
function loadAllCommands(rootDir, options = {}) {
|
|
391
|
+
return loadPromptFiles(
|
|
392
|
+
rootDir,
|
|
393
|
+
'commands',
|
|
394
|
+
f => f.endsWith('.md') && f.toLowerCase() !== 'readme.md',
|
|
395
|
+
(dir, file) => path.join(dir, file),
|
|
396
|
+
options
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Load all skill files from a directory structure
|
|
402
|
+
* @param {string} rootDir - Root directory to scan
|
|
403
|
+
* @param {Object} options - Options including verbose flag
|
|
404
|
+
* @returns {Array<Object>} Array of parsed skill objects
|
|
405
|
+
*/
|
|
406
|
+
function loadAllSkills(rootDir, options = {}) {
|
|
407
|
+
const skills = [];
|
|
408
|
+
const pluginsDir = path.join(rootDir, 'plugins');
|
|
409
|
+
|
|
410
|
+
// Validate path
|
|
411
|
+
if (!isPathWithinRoot(pluginsDir, rootDir)) {
|
|
412
|
+
return skills;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
fs.accessSync(pluginsDir);
|
|
417
|
+
} catch {
|
|
418
|
+
return skills;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
let plugins;
|
|
422
|
+
try {
|
|
423
|
+
plugins = fs.readdirSync(pluginsDir).filter(f => {
|
|
424
|
+
const fullPath = path.join(pluginsDir, f);
|
|
425
|
+
if (!isPathWithinRoot(fullPath, rootDir)) return false;
|
|
426
|
+
try {
|
|
427
|
+
return fs.statSync(fullPath).isDirectory();
|
|
428
|
+
} catch {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
} catch (err) {
|
|
433
|
+
if (options.verbose) {
|
|
434
|
+
console.error('[cross-file] Failed to read plugins directory:', err.message);
|
|
435
|
+
}
|
|
436
|
+
return skills;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
for (const plugin of plugins) {
|
|
440
|
+
const skillsDir = path.join(pluginsDir, plugin, 'skills');
|
|
441
|
+
|
|
442
|
+
if (!isPathWithinRoot(skillsDir, rootDir)) continue;
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
fs.accessSync(skillsDir);
|
|
446
|
+
} catch {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
let skillDirs;
|
|
451
|
+
try {
|
|
452
|
+
skillDirs = fs.readdirSync(skillsDir).filter(f => {
|
|
453
|
+
const fullPath = path.join(skillsDir, f);
|
|
454
|
+
if (!isPathWithinRoot(fullPath, rootDir)) return false;
|
|
455
|
+
try {
|
|
456
|
+
return fs.statSync(fullPath).isDirectory();
|
|
457
|
+
} catch {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
} catch (err) {
|
|
462
|
+
if (options.verbose) {
|
|
463
|
+
console.error('[cross-file] Failed to read skills directory:', skillsDir, err.message);
|
|
464
|
+
}
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
for (const skillDir of skillDirs) {
|
|
469
|
+
const skillPath = path.join(skillsDir, skillDir, 'SKILL.md');
|
|
470
|
+
|
|
471
|
+
if (!isPathWithinRoot(skillPath, rootDir)) continue;
|
|
472
|
+
|
|
473
|
+
try {
|
|
474
|
+
fs.accessSync(skillPath);
|
|
475
|
+
const content = fs.readFileSync(skillPath, 'utf8');
|
|
476
|
+
const { frontmatter, body } = parseMarkdownFrontmatter(content);
|
|
477
|
+
|
|
478
|
+
skills.push({
|
|
479
|
+
plugin,
|
|
480
|
+
name: skillDir,
|
|
481
|
+
path: skillPath,
|
|
482
|
+
frontmatter: frontmatter || {},
|
|
483
|
+
body,
|
|
484
|
+
content
|
|
485
|
+
});
|
|
486
|
+
} catch (err) {
|
|
487
|
+
if (options.verbose) {
|
|
488
|
+
console.error('[cross-file] Failed to read skill:', skillPath, err.message);
|
|
489
|
+
}
|
|
490
|
+
// Skip files that can't be read - intentional for robustness
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return skills;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ============================================
|
|
499
|
+
// ANALYSIS FUNCTIONS
|
|
500
|
+
// ============================================
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Analyze tool consistency between frontmatter and body
|
|
504
|
+
* @param {Array<Object>} agents - Parsed agents
|
|
505
|
+
* @param {string[]} knownTools - Known tool names
|
|
506
|
+
* @returns {Array<Object>} Findings
|
|
507
|
+
*/
|
|
508
|
+
function analyzeToolConsistency(agents, knownTools) {
|
|
509
|
+
const findings = [];
|
|
510
|
+
const pattern = crossFilePatterns.tool_not_in_allowed_list;
|
|
511
|
+
|
|
512
|
+
for (const agent of agents) {
|
|
513
|
+
const { frontmatter, body, name, path: agentPath } = agent;
|
|
514
|
+
|
|
515
|
+
// Get declared tools from frontmatter
|
|
516
|
+
let declaredTools = [];
|
|
517
|
+
if (frontmatter && frontmatter.tools) {
|
|
518
|
+
declaredTools = Array.isArray(frontmatter.tools)
|
|
519
|
+
? frontmatter.tools
|
|
520
|
+
: frontmatter.tools.split(',').map(t => t.trim());
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Skip if no tools restriction (all tools allowed)
|
|
524
|
+
if (declaredTools.length === 0) continue;
|
|
525
|
+
|
|
526
|
+
// Extract used tools from body
|
|
527
|
+
const usedTools = extractToolMentions(body, knownTools);
|
|
528
|
+
|
|
529
|
+
const result = pattern.check({
|
|
530
|
+
declaredTools,
|
|
531
|
+
usedTools,
|
|
532
|
+
agentName: name
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
if (result) {
|
|
536
|
+
findings.push({
|
|
537
|
+
...result,
|
|
538
|
+
file: agentPath,
|
|
539
|
+
certainty: pattern.certainty,
|
|
540
|
+
patternId: pattern.id,
|
|
541
|
+
source: 'cross-file'
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return findings;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Analyze workflow completeness (referenced agents exist)
|
|
551
|
+
* @param {Array<Object>} agents - Parsed agents
|
|
552
|
+
* @returns {Array<Object>} Findings
|
|
553
|
+
*/
|
|
554
|
+
function analyzeWorkflowCompleteness(agents) {
|
|
555
|
+
const findings = [];
|
|
556
|
+
const pattern = crossFilePatterns.missing_workflow_agent;
|
|
557
|
+
|
|
558
|
+
// Build list of existing agents
|
|
559
|
+
const existingAgents = agents.map(a => ({
|
|
560
|
+
plugin: a.plugin,
|
|
561
|
+
name: a.name
|
|
562
|
+
}));
|
|
563
|
+
|
|
564
|
+
// Check each agent for references to other agents
|
|
565
|
+
for (const agent of agents) {
|
|
566
|
+
const { body, path: agentPath } = agent;
|
|
567
|
+
const references = extractAgentReferences(body);
|
|
568
|
+
|
|
569
|
+
for (const ref of references) {
|
|
570
|
+
const result = pattern.check({
|
|
571
|
+
referencedAgent: ref,
|
|
572
|
+
existingAgents,
|
|
573
|
+
sourceFile: path.basename(agentPath)
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
if (result) {
|
|
577
|
+
findings.push({
|
|
578
|
+
...result,
|
|
579
|
+
file: agentPath,
|
|
580
|
+
certainty: pattern.certainty,
|
|
581
|
+
patternId: pattern.id,
|
|
582
|
+
source: 'cross-file'
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return findings;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Analyze prompt consistency (duplicates, contradictions)
|
|
593
|
+
* Uses optimized O(n) contradiction detection via keyword indexing
|
|
594
|
+
* @param {Array<Object>} agents - Parsed agents
|
|
595
|
+
* @returns {Array<Object>} Findings
|
|
596
|
+
*/
|
|
597
|
+
function analyzePromptConsistency(agents) {
|
|
598
|
+
const findings = [];
|
|
599
|
+
|
|
600
|
+
// Collect all critical instructions with their sources
|
|
601
|
+
const instructionMap = new Map(); // normalized instruction -> [files]
|
|
602
|
+
|
|
603
|
+
for (const agent of agents) {
|
|
604
|
+
const instructions = extractCriticalInstructions(agent.body);
|
|
605
|
+
|
|
606
|
+
for (const { line } of instructions) {
|
|
607
|
+
const normalized = line.toLowerCase().trim();
|
|
608
|
+
if (normalized.length < MIN_INSTRUCTION_LENGTH) continue;
|
|
609
|
+
|
|
610
|
+
if (!instructionMap.has(normalized)) {
|
|
611
|
+
instructionMap.set(normalized, []);
|
|
612
|
+
}
|
|
613
|
+
instructionMap.get(normalized).push(agent.path);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Find duplicates
|
|
618
|
+
const duplicatePattern = crossFilePatterns.duplicate_instructions;
|
|
619
|
+
for (const [instruction, files] of instructionMap.entries()) {
|
|
620
|
+
if (files.length >= MIN_DUPLICATE_COUNT) {
|
|
621
|
+
const result = duplicatePattern.check({ instruction, files });
|
|
622
|
+
if (result) {
|
|
623
|
+
findings.push({
|
|
624
|
+
...result,
|
|
625
|
+
file: files[0],
|
|
626
|
+
certainty: duplicatePattern.certainty,
|
|
627
|
+
patternId: duplicatePattern.id,
|
|
628
|
+
source: 'cross-file'
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Find contradictions using keyword indexing (O(n) instead of O(n^2))
|
|
635
|
+
const contradictionPattern = crossFilePatterns.contradictory_rules;
|
|
636
|
+
|
|
637
|
+
// Index rules by keywords for efficient lookup
|
|
638
|
+
const alwaysRulesByKeyword = new Map(); // keyword -> [{rule, file}]
|
|
639
|
+
const neverRulesByKeyword = new Map();
|
|
640
|
+
|
|
641
|
+
for (const agent of agents) {
|
|
642
|
+
const instructions = extractCriticalInstructions(agent.body);
|
|
643
|
+
|
|
644
|
+
for (const { line } of instructions) {
|
|
645
|
+
const isAlways = /\bALWAYS\b/i.test(line);
|
|
646
|
+
const isNever = /\bNEVER\b/i.test(line) || /\bDO NOT\b/i.test(line);
|
|
647
|
+
|
|
648
|
+
if (!isAlways && !isNever) continue;
|
|
649
|
+
|
|
650
|
+
// Extract action keywords
|
|
651
|
+
let action;
|
|
652
|
+
if (isAlways) {
|
|
653
|
+
action = line.replace(/.*\bALWAYS\b\s*/i, '').substring(0, ACTION_COMPARISON_LENGTH);
|
|
654
|
+
} else {
|
|
655
|
+
action = line.replace(/.*\b(?:NEVER|DO NOT)\b\s*/i, '').substring(0, ACTION_COMPARISON_LENGTH);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Extract significant keywords from action
|
|
659
|
+
const keywords = action.toLowerCase().split(/\s+/).filter(w => w.length >= MIN_WORD_LENGTH);
|
|
660
|
+
|
|
661
|
+
const ruleData = { line, file: agent.path, action: action.toLowerCase() };
|
|
662
|
+
const targetMap = isAlways ? alwaysRulesByKeyword : neverRulesByKeyword;
|
|
663
|
+
|
|
664
|
+
for (const keyword of keywords) {
|
|
665
|
+
if (!targetMap.has(keyword)) {
|
|
666
|
+
targetMap.set(keyword, []);
|
|
667
|
+
}
|
|
668
|
+
targetMap.get(keyword).push(ruleData);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Find contradictions by checking overlapping keywords
|
|
674
|
+
const checkedPairs = new Set();
|
|
675
|
+
|
|
676
|
+
for (const [keyword, alwaysRules] of alwaysRulesByKeyword.entries()) {
|
|
677
|
+
const neverRules = neverRulesByKeyword.get(keyword);
|
|
678
|
+
if (!neverRules) continue;
|
|
679
|
+
|
|
680
|
+
for (const always of alwaysRules) {
|
|
681
|
+
for (const never of neverRules) {
|
|
682
|
+
// Skip same file
|
|
683
|
+
if (always.file === never.file) continue;
|
|
684
|
+
|
|
685
|
+
// Skip already checked pairs
|
|
686
|
+
const pairKey = `${always.line}|${never.line}`;
|
|
687
|
+
if (checkedPairs.has(pairKey)) continue;
|
|
688
|
+
checkedPairs.add(pairKey);
|
|
689
|
+
|
|
690
|
+
// Check similarity
|
|
691
|
+
const similarity = calculateSimilarity(always.action, never.action);
|
|
692
|
+
if (similarity > CONTRADICTION_SIMILARITY_THRESHOLD) {
|
|
693
|
+
const result = contradictionPattern.check({
|
|
694
|
+
rule1: always.line,
|
|
695
|
+
rule2: never.line,
|
|
696
|
+
file1: path.basename(always.file),
|
|
697
|
+
file2: path.basename(never.file)
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
if (result) {
|
|
701
|
+
findings.push({
|
|
702
|
+
...result,
|
|
703
|
+
file: always.file,
|
|
704
|
+
certainty: contradictionPattern.certainty,
|
|
705
|
+
patternId: contradictionPattern.id,
|
|
706
|
+
source: 'cross-file'
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return findings;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Analyze skill-agent alignment
|
|
719
|
+
* @param {Array<Object>} skills - Parsed skills
|
|
720
|
+
* @param {string[]} knownTools - Known tool names
|
|
721
|
+
* @returns {Array<Object>} Findings
|
|
722
|
+
*/
|
|
723
|
+
function analyzeSkillAlignment(skills, knownTools) {
|
|
724
|
+
const findings = [];
|
|
725
|
+
const pattern = crossFilePatterns.skill_tool_mismatch;
|
|
726
|
+
|
|
727
|
+
for (const skill of skills) {
|
|
728
|
+
const { frontmatter, body, name, path: skillPath } = skill;
|
|
729
|
+
|
|
730
|
+
// Get allowed-tools from frontmatter (supports multiple field names)
|
|
731
|
+
let allowedTools = [];
|
|
732
|
+
const toolsField = frontmatter['allowed-tools'] || frontmatter.allowedTools || frontmatter.tools;
|
|
733
|
+
if (toolsField) {
|
|
734
|
+
allowedTools = Array.isArray(toolsField)
|
|
735
|
+
? toolsField
|
|
736
|
+
: toolsField.split(',').map(t => t.trim());
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Skip if no tools restriction
|
|
740
|
+
if (allowedTools.length === 0) continue;
|
|
741
|
+
|
|
742
|
+
// Extract used tools from body
|
|
743
|
+
const usedTools = extractToolMentions(body, knownTools);
|
|
744
|
+
|
|
745
|
+
const result = pattern.check({
|
|
746
|
+
skillName: name,
|
|
747
|
+
skillAllowedTools: allowedTools,
|
|
748
|
+
promptUsedTools: usedTools
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
if (result) {
|
|
752
|
+
findings.push({
|
|
753
|
+
...result,
|
|
754
|
+
file: skillPath,
|
|
755
|
+
certainty: pattern.certainty,
|
|
756
|
+
patternId: pattern.id,
|
|
757
|
+
source: 'cross-file'
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return findings;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Find orphaned agents (not referenced anywhere)
|
|
767
|
+
* @param {Array<Object>} agents - Parsed agents
|
|
768
|
+
* @param {Array<Object>} skills - Parsed skills
|
|
769
|
+
* @param {Array<Object>} commands - Parsed commands
|
|
770
|
+
* @returns {Array<Object>} Findings
|
|
771
|
+
*/
|
|
772
|
+
function analyzeOrphanedPrompts(agents, skills, commands = []) {
|
|
773
|
+
const findings = [];
|
|
774
|
+
const pattern = crossFilePatterns.orphaned_prompt;
|
|
775
|
+
|
|
776
|
+
// Collect all agent references
|
|
777
|
+
const allReferences = new Set();
|
|
778
|
+
|
|
779
|
+
// From agents
|
|
780
|
+
for (const agent of agents) {
|
|
781
|
+
const refs = extractAgentReferences(agent.body);
|
|
782
|
+
refs.forEach(r => allReferences.add(r));
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// From skills
|
|
786
|
+
for (const skill of skills) {
|
|
787
|
+
const refs = extractAgentReferences(skill.body);
|
|
788
|
+
refs.forEach(r => allReferences.add(r));
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// From commands (addresses false positives for command-invoked agents)
|
|
792
|
+
for (const command of commands) {
|
|
793
|
+
const refs = extractAgentReferences(command.body);
|
|
794
|
+
refs.forEach(r => allReferences.add(r));
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Check each agent
|
|
798
|
+
for (const agent of agents) {
|
|
799
|
+
const fullName = `${agent.plugin}:${agent.name}`;
|
|
800
|
+
const shortName = agent.name;
|
|
801
|
+
|
|
802
|
+
// Check if referenced
|
|
803
|
+
const isReferenced = allReferences.has(fullName) || allReferences.has(shortName);
|
|
804
|
+
|
|
805
|
+
// Entry point agents are typically called by commands, not other agents
|
|
806
|
+
const isEntryPoint = /orchestrator|discoverer|validator|monitor|fixer|checker|reporter|manager/i.test(agent.name);
|
|
807
|
+
|
|
808
|
+
if (!isReferenced && !isEntryPoint) {
|
|
809
|
+
const result = pattern.check({
|
|
810
|
+
promptFile: path.basename(agent.path),
|
|
811
|
+
referencedBy: []
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
if (result) {
|
|
815
|
+
findings.push({
|
|
816
|
+
...result,
|
|
817
|
+
file: agent.path,
|
|
818
|
+
certainty: pattern.certainty,
|
|
819
|
+
patternId: pattern.id,
|
|
820
|
+
source: 'cross-file'
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
return findings;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// ============================================
|
|
830
|
+
// MAIN ANALYSIS FUNCTION
|
|
831
|
+
// ============================================
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Main cross-file analysis function
|
|
835
|
+
* @param {string} rootDir - Root directory to analyze
|
|
836
|
+
* @param {Object} options - Analysis options
|
|
837
|
+
* @param {boolean} options.verbose - Include all findings and log errors
|
|
838
|
+
* @param {string[]} options.categories - Specific categories to check
|
|
839
|
+
* @returns {Object} Analysis results
|
|
840
|
+
*/
|
|
841
|
+
function analyze(rootDir, options = {}) {
|
|
842
|
+
const {
|
|
843
|
+
verbose = false,
|
|
844
|
+
categories = DEFAULT_ANALYSIS_CATEGORIES
|
|
845
|
+
} = options;
|
|
846
|
+
|
|
847
|
+
const results = {
|
|
848
|
+
rootDir,
|
|
849
|
+
findings: [],
|
|
850
|
+
summary: {
|
|
851
|
+
agentsAnalyzed: 0,
|
|
852
|
+
skillsAnalyzed: 0,
|
|
853
|
+
totalFindings: 0,
|
|
854
|
+
byCategory: {}
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
// Load known tools (config file or platform defaults)
|
|
859
|
+
const knownTools = loadKnownTools(rootDir);
|
|
860
|
+
|
|
861
|
+
// Load all agents, skills, and commands
|
|
862
|
+
const agents = loadAllAgents(rootDir, { verbose });
|
|
863
|
+
const skills = loadAllSkills(rootDir, { verbose });
|
|
864
|
+
const commands = loadAllCommands(rootDir, { verbose });
|
|
865
|
+
|
|
866
|
+
results.summary.agentsAnalyzed = agents.length;
|
|
867
|
+
results.summary.skillsAnalyzed = skills.length;
|
|
868
|
+
results.summary.commandsAnalyzed = commands.length;
|
|
869
|
+
|
|
870
|
+
// Run analysis for each category
|
|
871
|
+
if (categories.includes('tool-consistency')) {
|
|
872
|
+
const toolFindings = analyzeToolConsistency(agents, knownTools);
|
|
873
|
+
results.findings.push(...toolFindings);
|
|
874
|
+
results.summary.byCategory['tool-consistency'] = toolFindings.length;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
if (categories.includes('workflow')) {
|
|
878
|
+
const workflowFindings = analyzeWorkflowCompleteness(agents);
|
|
879
|
+
results.findings.push(...workflowFindings);
|
|
880
|
+
results.summary.byCategory['workflow'] = workflowFindings.length;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (categories.includes('consistency')) {
|
|
884
|
+
const consistencyFindings = analyzePromptConsistency(agents);
|
|
885
|
+
const orphanFindings = analyzeOrphanedPrompts(agents, skills, commands);
|
|
886
|
+
results.findings.push(...consistencyFindings, ...orphanFindings);
|
|
887
|
+
results.summary.byCategory['consistency'] = consistencyFindings.length + orphanFindings.length;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (categories.includes('skill-alignment')) {
|
|
891
|
+
const skillFindings = analyzeSkillAlignment(skills, knownTools);
|
|
892
|
+
results.findings.push(...skillFindings);
|
|
893
|
+
results.summary.byCategory['skill-alignment'] = skillFindings.length;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
results.summary.totalFindings = results.findings.length;
|
|
897
|
+
|
|
898
|
+
return results;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// ============================================
|
|
902
|
+
// EXPORTS
|
|
903
|
+
// ============================================
|
|
904
|
+
|
|
905
|
+
module.exports = {
|
|
906
|
+
// Extraction functions
|
|
907
|
+
extractToolMentions,
|
|
908
|
+
extractAgentReferences,
|
|
909
|
+
extractCriticalInstructions,
|
|
910
|
+
|
|
911
|
+
// Loading functions
|
|
912
|
+
loadAllAgents,
|
|
913
|
+
loadAllSkills,
|
|
914
|
+
loadAllCommands,
|
|
915
|
+
|
|
916
|
+
// Analysis functions
|
|
917
|
+
analyzeToolConsistency,
|
|
918
|
+
analyzeWorkflowCompleteness,
|
|
919
|
+
analyzePromptConsistency,
|
|
920
|
+
analyzeSkillAlignment,
|
|
921
|
+
analyzeOrphanedPrompts,
|
|
922
|
+
|
|
923
|
+
// Main entry point
|
|
924
|
+
analyze,
|
|
925
|
+
|
|
926
|
+
// Utilities (exported for testing)
|
|
927
|
+
calculateSimilarity,
|
|
928
|
+
escapeRegex,
|
|
929
|
+
isPathWithinRoot
|
|
930
|
+
};
|